functions_framework 0.4.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,7 +14,8 @@
14
14
 
15
15
  require "logger"
16
16
 
17
- require "functions_framework/cloud_events"
17
+ require "cloud_events"
18
+
18
19
  require "functions_framework/function"
19
20
  require "functions_framework/legacy_event_converter"
20
21
  require "functions_framework/registry"
@@ -44,10 +45,6 @@ require "functions_framework/version"
44
45
  #
45
46
  # Here is a roadmap to the internal modules in the Ruby functions framework.
46
47
  #
47
- # * {FunctionsFramework::CloudEvents} provides an implementation of the
48
- # [CloudEvents](https://cloudevents.io) specification. In particular, if
49
- # you define an event function, you will receive the event as a
50
- # {FunctionsFramework::CloudEvents::Event} object.
51
48
  # * {FunctionsFramework::CLI} is the implementation of the
52
49
  # `functions-framework-ruby` executable. Most apps will not need to interact
53
50
  # with this class directly.
@@ -74,7 +71,7 @@ require "functions_framework/version"
74
71
  #
75
72
  module FunctionsFramework
76
73
  @global_registry = Registry.new
77
- @logger = ::Logger.new ::STDERR
74
+ @logger = ::Logger.new $stderr
78
75
  @logger.level = ::Logger::INFO
79
76
 
80
77
  ##
@@ -94,6 +91,12 @@ module FunctionsFramework
94
91
  #
95
92
  DEFAULT_SOURCE = "./app.rb".freeze
96
93
 
94
+ ##
95
+ # The CloudEvents implementation was extracted to become the official
96
+ # CloudEvents SDK. This alias is left here for backward compatibility.
97
+ #
98
+ CloudEvents = ::CloudEvents
99
+
97
100
  class << self
98
101
  ##
99
102
  # The "global" registry that holds events defined by the
@@ -144,7 +147,8 @@ module FunctionsFramework
144
147
  #
145
148
  # You must provide a name for the function, and a block that implemets the
146
149
  # function. The block should take one argument: the event object of type
147
- # {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
150
+ # [`CloudEvents::Event`](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event).
151
+ # Any return value is ignored.
148
152
  #
149
153
  # ## Example
150
154
  #
@@ -162,8 +166,30 @@ module FunctionsFramework
162
166
  end
163
167
 
164
168
  ##
165
- # Start the functions framework server in the background. The server will
166
- # 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.
167
193
  #
168
194
  # @param target [FunctionsFramework::Function,String] The function to run,
169
195
  # or the name of the function to look up in the global registry.
@@ -179,7 +205,12 @@ module FunctionsFramework
179
205
  function = global_registry[target]
180
206
  raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
181
207
  end
182
- 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
183
214
  server.respond_to_signals
184
215
  server.start
185
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.
@@ -52,7 +75,7 @@ module FunctionsFramework
52
75
  # @return [self]
53
76
  #
54
77
  def parse_args argv # rubocop:disable Metrics/MethodLength
55
- option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
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,43 +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
- # {FunctionsFramework::CloudEvents::Event CloudEvent} argument, and does not
27
- # return a value. 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}.
28
36
  #
29
- # If a callable object is provided directly, its `call` method is invoked for
30
- # every function execution. Note that this means it may be called multiple
31
- # times concurrently in separate threads.
37
+ # The implementation can be specified in one of three ways:
32
38
  #
33
- # Alternately, the implementation may be provided as a class that should be
34
- # instantiated to produce a callable object. If a class is provided, it should
35
- # either subclass {FunctionsFramework::Function::CallBase} or respond to the
36
- # same constructor interface, i.e. accepting arbitrary keyword arguments. A
37
- # separate callable object will be instantiated from this class for every
38
- # 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.)
39
52
  #
40
- # Finally, an implementation can be provided as a block. If a block is
41
- # provided, it will be recast as a `call` method in an anonymous subclass of
42
- # {FunctionsFramework::Function::CallBase}. Thus, providing a block is really
43
- # just syntactic sugar for providing a class. (This means, for example, that
44
- # the `return` keyword will work within the block because it is treated as a
45
- # 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.
46
59
  #
47
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
+
48
96
  ##
49
97
  # Create a new function definition.
50
98
  #
51
99
  # @param name [String] The function name
52
- # @param type [Symbol] The type of function. Valid types are `:http` and
53
- # `:cloud_event`.
100
+ # @param type [Symbol] The type of function. Valid types are `:http`,
101
+ # `:cloud_event`, and `:startup_task`.
54
102
  # @param callable [Class,#call] A callable object or class.
55
103
  # @param block [Proc] The function code as a block.
56
104
  #
57
- def initialize name, type, callable = nil, &block
105
+ def initialize name, type, callable: nil, &block
58
106
  @name = name
59
107
  @type = type
60
108
  @callable = @callable_class = nil
@@ -63,7 +111,7 @@ module FunctionsFramework
63
111
  elsif callable.is_a? ::Class
64
112
  @callable_class = callable
65
113
  elsif block_given?
66
- @callable_class = ::Class.new CallBase do
114
+ @callable_class = ::Class.new Callable do
67
115
  define_method :call, &block
68
116
  end
69
117
  else
@@ -82,18 +130,38 @@ module FunctionsFramework
82
130
  attr_reader :type
83
131
 
84
132
  ##
85
- # Get a callable for performing a function invocation. This will either
86
- # return the singleton callable object, or instantiate a new callable from
87
- # the configured class.
88
- #
89
- # @param logger [::Logger] The logger for use by function executions. This
90
- # may or may not be used by the callable.
91
- # @return [#call]
92
- #
93
- def new_call logger: nil
94
- return @callable unless @callable.nil?
95
- logger ||= FunctionsFramework.logger
96
- @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)
97
165
  end
98
166
 
99
167
  ##
@@ -101,29 +169,56 @@ module FunctionsFramework
101
169
  #
102
170
  # An object of this class is `self` while a function block is running.
103
171
  #
104
- class CallBase
172
+ class Callable
105
173
  ##
106
174
  # Create a callable object with the given context.
107
175
  #
108
- # @param context [keywords] A set of context arguments. See {#context} for
109
- # a list of keys that will generally be passed in. However,
110
- # 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.
111
178
  #
112
- def initialize **context
113
- @context = context
179
+ def initialize globals: nil, logger: nil
180
+ @__globals = globals || {}
181
+ @__logger = logger || FunctionsFramework.logger
114
182
  end
115
183
 
116
184
  ##
117
- # 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:
118
188
  #
119
- # * **:logger** (`Logger`) A logger for use by this function call.
120
189
  # * **:function_name** (`String`) The name of the running function.
121
190
  # * **:function_type** (`Symbol`) The type of the running function,
122
191
  # either `:http` or `:cloud_event`.
123
192
  #
124
- # @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]
125
197
  #
126
- 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
127
222
  end
128
223
  end
129
224
  end