Skip to content

Commit c011260

Browse files
authored
feat: init binding and async (#1)
* test: fix unit tests (#181) Fix failing assert to look for a different error message, likely due to a change in CloudEvents error message. Also add tests so that code coverage is back at 100%. Signed-off-by: Kehui Li <[email protected]>
1 parent 9046388 commit c011260

25 files changed

+818
-37
lines changed

Diff for: README.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55
[![Python unit CI][ff_python_unit_img]][ff_python_unit_link] [![Python lint CI][ff_python_lint_img]][ff_python_lint_link] [![Python conformace CI][ff_python_conformance_img]][ff_python_conformance_link]
66

77
An open source FaaS (Function as a service) framework for writing portable
8-
Python functions -- brought to you by the Google Cloud Functions team.
8+
Python functions.
99

1010
The Functions Framework lets you write lightweight functions that run in many
1111
different environments, including:
1212

13+
* [OpenFunction](https://github.com/OpenFunction/OpenFunction)
14+
* [Knative](https://github.com/knative/)-based environments
15+
* [Dapr](https://dapr.io/)-based environments
1316
* [Google Cloud Functions](https://cloud.google.com/functions/)
1417
* Your local development machine
1518
* [Cloud Run and Cloud Run for Anthos](https://cloud.google.com/run/)
16-
* [Knative](https://github.com/knative/)-based environments
1719

1820
The framework allows you to go from:
1921

@@ -292,6 +294,22 @@ https://cloud.google.com/functions/docs/tutorials/pubsub#functions_helloworld_pu
292294

293295
## Run your function on serverless platforms
294296

297+
### Container environments based on Knative
298+
299+
The Functions Framework is designed to be compatible with Knative environments. Build and deploy your container to a Knative environment.
300+
301+
### OpenFunction
302+
303+
![OpenFunction Platform Overview](https://openfunction.dev/openfunction-0.5-architecture.png)
304+
305+
Besides Knative function support, one notable feature of OpenFunction is embracing Dapr system, so far Dapr pub/sub and bindings have been support.
306+
307+
Dapr bindings allows you to trigger your applications or services with events coming in from external systems, or interface with external systems. OpenFunction [0.6.0 release](https://openfunction.dev/blog/2022/03/25/announcing-openfunction-0.6.0-faas-observability-http-trigger-and-more/) adds Dapr output bindings to its synchronous functions which enables HTTP triggers for asynchronous functions. For example, synchronous functions backed by the Knative runtime can now interact with middlewares defined by Dapr output binding or pub/sub, and an asynchronous function will be triggered by the events sent from the synchronous function.
308+
309+
Asynchronous function introduces Dapr pub/sub to provide a platform-agnostic API to send and receive messages. A typical use case is that you can leverage synchronous functions to receive an event in plain JSON or Cloud Events format, and then send the received event to a Dapr output binding or pub/sub component, most likely a message queue (e.g. Kafka, NATS Streaming, GCP PubSub, MQTT). Finally, the asynchronous function could be triggered from the message queue.
310+
311+
More details would be brought up to you in some quickstart samples, stay tuned.
312+
295313
### Google Cloud Functions
296314

297315
This Functions Framework is based on the [Python Runtime on Google Cloud Functions](https://cloud.google.com/functions/docs/concepts/python-runtime).

Diff for: docs/async-server.puml

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
@startuml Async Server
2+
3+
box Function Process in Local Environment or Container
4+
control ENTRYPOINT
5+
participant "~__main__" as Main
6+
participant AsyncServer
7+
participant DaprServer
8+
participant gRPCServer [
9+
Web Server
10+
----
11+
""gprc.server""
12+
]
13+
end box
14+
15+
entity "Dapr Sidecar " as DaprSidecar
16+
17+
== OpenFunction Serving ==
18+
19+
ENTRYPOINT -> Main ** : execute
20+
note over ENTRYPOINT, Main: Pass through __CLI arguments__ and \ncontainer __environment variables__
21+
22+
Main -> Main : load user function file
23+
note left: ""function (ctx, data) {}""
24+
25+
Main -> AsyncServer ** : create
26+
note over Main, AsyncServer: Hand over __user function__ and __context__
27+
28+
AsyncServer -> DaprServer ** : ""new""
29+
note over AsyncServer, DaprServer: Extract __port__ from __context__ and pass down
30+
31+
DaprServer -> gRPCServer ** : ""new""
32+
|||
33+
DaprServer --> DaprSidecar : Waiting till Dapr sidecar started
34+
...
35+
AsyncServer -> DaprServer : register __user function__ as handler \nfor each of __inputs__ in __context__
36+
DaprServer -> gRPCServer : add routes for Dapr style \nsubscriptions and input bindings
37+
38+
...
39+
40+
== OpenFunction Triggering ==
41+
42+
DaprSidecar <-- : sub / input data
43+
44+
DaprSidecar -> gRPCServer ++ : Dapr request with "data"
45+
46+
gRPCServer -> gRPCServer ++ : invoke user function
47+
48+
alt
49+
gRPCServer -> DaprSidecar ++ : publish data or invoke output binding
50+
DaprSidecar --> gRPCServer -- : execution result
51+
end
52+
53+
return
54+
55+
return server app response
56+
57+
...
58+
59+
@enduml

Diff for: docs/http-binding.puml

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
@startuml HTTP Binding
2+
3+
box Function Process in Local Environment or Container
4+
control ENTRYPOINT
5+
participant "~__main__" as Main
6+
participant HTTPServer
7+
participant Server [
8+
Web Server
9+
----
10+
""Flask/Gunicorn""
11+
]
12+
participant Middleware
13+
participant "User Function" as UserFunction
14+
participant DaprClient
15+
end box
16+
17+
entity "Dapr Sidecar " as DaprSidecar
18+
19+
== OpenFunction Serving ==
20+
21+
ENTRYPOINT -> Main ** : execute
22+
note over ENTRYPOINT, Main: Pass through __CLI arguments__ and \ncontainer __environment variables__
23+
24+
Main -> Main : load user fnction file
25+
note left: ""function (request) {}""
26+
27+
Main -> HTTPServer ** : create
28+
note over Main, HTTPServer: Hand over __user function__, __function type__ \nand __context__ parsed from env variables
29+
30+
HTTPServer -> Server ** : new
31+
note over Server: Depend on debug mode
32+
33+
HTTPServer -> Middleware ** : new
34+
HTTPServer -> Server : use Middleware
35+
note over HTTPServer, Server: Pass context to middleware
36+
|||
37+
HTTPServer -> Server : use others middlewares
38+
|||
39+
HTTPServer -> UserFunction ** : wrap user function
40+
note over HTTPServer, UserFunction: Register as HTTP or CloudEvent Function
41+
HTTPServer -> Server : bind wrapper to "/*" route
42+
43+
...
44+
45+
== OpenFunction Invocation ==
46+
47+
[-> Server ++ : HTTP request to "/"
48+
49+
Server -> UserFunction ++ : execute user function
50+
UserFunction --> Server -- : return execution result "data"
51+
52+
alt ""runtime"" = ""knative"" and ""outputs"" is not empty
53+
Server -> Middleware ++ : invoke Middleware
54+
55+
Middleware -> DaprClient ** : new
56+
57+
loop each OpenFunction Output
58+
Middleware -> DaprClient ++ : send "data"
59+
60+
DaprClient -> DaprSidecar ++ : invoke binding or publication with "data"
61+
DaprSidecar --> DaprClient -- : return result
62+
63+
DaprClient --> Middleware -- : forward result
64+
end
65+
66+
Middleware --> Server -- : return "data" as response
67+
end
68+
69+
[<- Server -- : send response
70+
71+
...
72+
73+
@enduml

Diff for: setup.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@
2525

2626
setup(
2727
name="functions-framework",
28-
version="3.0.0",
29-
description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.",
28+
version="3.1.0",
29+
description="An open source FaaS (Function as a service) framework for writing portable Python functions.",
3030
long_description=long_description,
3131
long_description_content_type="text/markdown",
32-
url="https://github.com/googlecloudplatform/functions-framework-python",
33-
author="Google LLC",
34-
author_email="[email protected]",
32+
url="https://github.com/OpenFunction/functions-framework-python",
33+
author="OpenFunction",
34+
author_email="[email protected]",
3535
classifiers=[
3636
"Development Status :: 3 - Alpha",
3737
"Intended Audience :: Developers",
@@ -55,6 +55,7 @@
5555
"watchdog>=1.0.0,<2.0.0",
5656
"gunicorn>=19.2.0,<21.0; platform_system!='Windows'",
5757
"cloudevents>=1.2.0,<2.0.0",
58+
"dapr>=1.6.0",
5859
],
5960
entry_points={
6061
"console_scripts": [

Diff for: src/functions_framework/__init__.py

+40-5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
MissingSourceException,
3535
)
3636
from google.cloud.functions.context import Context
37+
from openfunction.dapr_output_middleware import dapr_output_middleware
38+
from openfunction.async_server import AsyncApp
3739

3840
MAX_CONTENT_LENGTH = 10 * 1024 * 1024
3941

@@ -94,6 +96,11 @@ def setup_logging():
9496
logging.getLogger().addHandler(warn_handler)
9597

9698

99+
def setup_logging_level(debug):
100+
if debug:
101+
logging.getLogger().setLevel(logging.DEBUG)
102+
103+
97104
def _http_view_func_wrapper(function, request):
98105
def view_func(path):
99106
return function(request._get_current_object())
@@ -175,7 +182,7 @@ def view_func(path):
175182
return view_func
176183

177184

178-
def _configure_app(app, function, signature_type):
185+
def _configure_app(app, function, signature_type, func_context):
179186
# Mount the function at the root. Support GCF's default path behavior
180187
# Modify the url_map and view_functions directly here instead of using
181188
# add_url_rule in order to create endpoints that route all methods
@@ -189,6 +196,7 @@ def _configure_app(app, function, signature_type):
189196
app.view_functions["run"] = _http_view_func_wrapper(function, flask.request)
190197
app.view_functions["error"] = lambda: flask.abort(404, description="Not Found")
191198
app.after_request(read_request)
199+
app.after_request(dapr_output_middleware(func_context))
192200
elif signature_type == _function_registry.BACKGROUNDEVENT_SIGNATURE_TYPE:
193201
app.url_map.add(
194202
werkzeug.routing.Rule(
@@ -241,8 +249,31 @@ def crash_handler(e):
241249
"""
242250
return str(e), 500, {_FUNCTION_STATUS_HEADER_FIELD: _CRASH}
243251

252+
def create_async_app(target=None, source=None, func_context=None, debug=False):
253+
target = _function_registry.get_function_target(target)
254+
source = _function_registry.get_function_source(source)
255+
256+
if not os.path.exists(source):
257+
raise MissingSourceException(
258+
"File {source} that is expected to define function doesn't exist".format(
259+
source=source
260+
)
261+
)
262+
263+
source_module, spec = _function_registry.load_function_module(source)
264+
spec.loader.exec_module(source_module)
265+
266+
function = _function_registry.get_user_function(source, source_module, target)
267+
268+
setup_logging_level(debug)
269+
270+
async_app = AsyncApp(func_context)
271+
async_app.bind(function)
272+
273+
return async_app.app
274+
244275

245-
def create_app(target=None, source=None, signature_type=None):
276+
def create_app(target=None, source=None, signature_type=None, func_context=None, debug=False):
246277
target = _function_registry.get_function_target(target)
247278
source = _function_registry.get_function_source(source)
248279

@@ -282,6 +313,8 @@ def handle_none(rv):
282313
sys.stdout = _LoggingHandler("INFO", sys.stderr)
283314
sys.stderr = _LoggingHandler("ERROR", sys.stderr)
284315
setup_logging()
316+
317+
setup_logging_level(debug)
285318

286319
# Execute the module, within the application context
287320
with _app.app_context():
@@ -291,7 +324,7 @@ def handle_none(rv):
291324
signature_type = _function_registry.get_func_signature_type(target, signature_type)
292325
function = _function_registry.get_user_function(source, source_module, target)
293326

294-
_configure_app(_app, function, signature_type)
327+
_configure_app(_app, function, signature_type, func_context)
295328

296329
return _app
297330

@@ -302,21 +335,23 @@ class LazyWSGIApp:
302335
at import-time
303336
"""
304337

305-
def __init__(self, target=None, source=None, signature_type=None):
338+
def __init__(self, target=None, source=None, signature_type=None, func_context=None, debug=False):
306339
# Support HTTP frameworks which support WSGI callables.
307340
# Note: this ability is currently broken in Gunicorn 20.0, and
308341
# environment variables should be used for configuration instead:
309342
# https://github.com/benoitc/gunicorn/issues/2159
310343
self.target = target
311344
self.source = source
312345
self.signature_type = signature_type
346+
self.func_context = func_context
347+
self.debug = debug
313348

314349
# Placeholder for the app which will be initialized on first call
315350
self.app = None
316351

317352
def __call__(self, *args, **kwargs):
318353
if not self.app:
319-
self.app = create_app(self.target, self.source, self.signature_type)
354+
self.app = create_app(self.target, self.source, self.signature_type, self.func_context, self.debug)
320355
return self.app(*args, **kwargs)
321356

322357

Diff for: src/functions_framework/_cli.py

+22-8
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
import click
1818

19-
from functions_framework import create_app
19+
from functions_framework import create_app, create_async_app
2020
from functions_framework._http import create_server
21-
21+
from functions_framework import _function_registry
2222

2323
@click.command()
2424
@click.option("--target", envvar="FUNCTION_TARGET", type=click.STRING, required=True)
@@ -34,10 +34,24 @@
3434
@click.option("--debug", envvar="DEBUG", is_flag=True)
3535
@click.option("--dry-run", envvar="DRY_RUN", is_flag=True)
3636
def _cli(target, source, signature_type, host, port, debug, dry_run):
37-
app = create_app(target, source, signature_type)
38-
if dry_run:
39-
click.echo("Function: {}".format(target))
40-
click.echo("URL: http://{}:{}/".format(host, port))
41-
click.echo("Dry run successful, shutting down.")
37+
context = _function_registry.get_openfunction_context(None)
38+
39+
# determine if async or knative
40+
if context and context.is_runtime_async():
41+
app = create_async_app(target, source, context, debug)
42+
if dry_run:
43+
run_dry(target, host, port)
44+
else:
45+
app.run(context.port)
4246
else:
43-
create_server(app, debug).run(host, port)
47+
app = create_app(target, source, signature_type, context, debug)
48+
if dry_run:
49+
run_dry(target, host, port)
50+
else:
51+
create_server(app, debug).run(host, port)
52+
53+
54+
def run_dry(target, host, port):
55+
click.echo("Function: {}".format(target))
56+
click.echo("URL: http://{}:{}/".format(host, port))
57+
click.echo("Dry run successful, shutting down.")

0 commit comments

Comments
 (0)