Python SDK

Stateful functions are the building blocks of applications; they are atomic units of isolation, distribution, and persistence. As objects, they encapsulate the state of a single entity (e.g., a specific user, device, or session) and encode its behavior. Stateful functions can interact with each other, and external systems, through message passing. The Python SDK is supported as a remote module.

To get started, add the Python SDK as a dependency to your application.

  1. apache-flink-statefun==2.2.0

Defining A Stateful Function

A stateful function is any function that that takes two parameters, a context and message. The function is bound to the runtime through the stateful functions decorator. The following is an example of a simple hello world function.

  1. from statefun import StatefulFunctions
  2. functions = StatefulFunctions()
  3. @functions.bind("example/hello")
  4. def hello_function(context, message):
  5. """A simple hello world function"""
  6. user = User()
  7. message.Unpack(user)
  8. print("Hello " + user.name)

This code declares a function with in the namespace example and of type hello and binds it to the hello_function Python instance.

Messages’s are untyped and passed through the system as google.protobuf.Any so one function can potentially process multiple types of messages.

The context provides metadata about the current message and function, and is how you can call other functions or external systems. A full reference of all methods supported by the context object are listed at the bottom of this page.

Type Hints

If the function has a static set of known supported types, they may be specified as type hints. This includes union types for functions that support multiple input message types.

  1. import typing
  2. from statefun import StatefulFunctions
  3. functions = StatefulFunctions()
  4. @functions.bind("example/hello")
  5. def hello_function(context, message: User):
  6. """A simple hello world function with typing"""
  7. print("Hello " + message.name)
  8. @function.bind("example/goodbye")
  9. def goodbye_function(context, message: typing.Union[User, Admin]):
  10. """A function that dispatches on types"""
  11. if isinstance(message, User):
  12. print("Goodbye user")
  13. elif isinstance(message, Admin):
  14. print("Goodbye Admin")

Function Types and Messaging

The decorator bind registers each function with the runtime under a function type. The function type must take the form <namespace>/<name>. Function types can then be referenced from other functions to create an address and message a particular instance.

  1. from google.protobuf.any_pb2 import Any
  2. from statefun import StatefulFunctions
  3. functions = StatefulFunctions()
  4. @functions.bind("example/caller")
  5. def caller_function(context, message):
  6. """A simple stateful function that sends a message to the user with id `user1`"""
  7. user = User()
  8. user.user_id = "user1"
  9. user.name = "Seth"
  10. envelope = Any()
  11. envelope.Pack(user)
  12. context.send("example/hello", user.user_id, envelope)

Alternatively, functions can be manually bound to the runtime.

  1. functions.register("example/caller", caller_function)

Sending Delayed Messages

Functions are able to send messages on a delay so that they will arrive after some duration. Functions may even send themselves delayed messages that can serve as a callback. The delayed message is non-blocking so functions will continue to process records between the time a delayed message is sent and received. The delay is specified via a Python timedelta.

  1. from datetime import timedelta
  2. from statefun import StatefulFunctions
  3. functions = StatefulFunctions()
  4. @functions.bind("example/delay")
  5. def delayed_function(context, message):
  6. """A simple stateful function that sends a message to its caller with a delay"""
  7. response = Response()
  8. response.message = "hello from the past"
  9. context.pack_and_send_after(
  10. context.caller.typename(),
  11. context.caller.identity,
  12. timedelta(minutes=30),
  13. response)

Persistence

Stateful Functions treats state as a first class citizen and so all stateful functions can easily define state that is automatically made fault tolerant by the runtime. All stateful functions may contain state by merely storing values within the context object. The data is always scoped to a specific function type and identifier. State values could be absent, None, or a google.protobuf.Any.

Attention: Remote modules require that all state values are eagerly registered at module.yaml. It’ll also allow configuring other state properties, such as state expiration. Please refer to that page for more details.

Below is a stateful function that greets users based on the number of times they have been seen.

  1. from google.protobuf.any_pb2 import Any
  2. from statefun import StatefulFunctions
  3. functions = StatefulFunctions()
  4. @functions.bind("example/count")
  5. def count_greeter(context, message):
  6. """Function that greets a user based on
  7. the number of times it has been called"""
  8. user = User()
  9. message.Unpack(user)
  10. state = context["count"]
  11. if state is None:
  12. state = Any()
  13. state.Pack(Count(1))
  14. output = generate_message(1, user)
  15. else:
  16. counter = Count()
  17. state.Unpack(counter)
  18. counter.value += 1
  19. output = generate_message(counter.value, user)
  20. state.Pack(counter)
  21. context["count"] = state
  22. print(output)
  23. def generate_message(count, user):
  24. if count == 1:
  25. return "Hello " + user.name
  26. elif count == 2:
  27. return "Hello again!"
  28. elif count == 3:
  29. return "Third time's the charm"
  30. else:
  31. return "Hello for the " + count + "th time"

Additionally, persisted values may be cleared by deleting its value.

  1. del context["count"]

Exposing Functions

The Python SDK ships with a RequestReplyHandler that automatically dispatches function calls based on RESTful HTTP POSTS. The RequestReplyHandler may be exposed using any HTTP framework.

  1. from statefun import RequestReplyHandler
  2. handler RequestReplyHandler(functions)

Serving Functions With Flask

One popular Python web framework is Flask. It can be used to quickly and easily expose a RequestReplyHandler.

  1. @app.route('/statefun', methods=['POST'])
  2. def handle():
  3. response_data = handler(request.data)
  4. response = make_response(response_data)
  5. response.headers.set('Content-Type', 'application/octet-stream')
  6. return response
  7. if __name__ == "__main__":
  8. app.run()

Serving Asynchronous Functions

The Python SDK ships with an additional handler, AsyncRequestReplyHandler, that supports Python’s awaitable functions (coroutines). This handler can be used with asynchronous Python frameworks, for example aiohttp.

  1. @functions.bind("example/hello")
  2. async def hello(context, message):
  3. response = await compute_greeting(message)
  4. context.reply(response)
  5. from aiohttp import web
  6. handler = AsyncRequestReplyHandler(functions)
  7. async def handle(request):
  8. req = await request.read()
  9. res = await handler(req)
  10. return web.Response(body=res, content_type="application/octet-stream")
  11. app = web.Application()
  12. app.add_routes([web.post('/statefun', handle)])
  13. if __name__ == '__main__':
  14. web.run_app(app, port=5000)

Context Reference

The context object passed to each function has the following attributes / methods.

  • address
    • The address of the current function under execution
  • caller
    • The address of the function that sent the current message. May be None if the message came from an ingress.
  • send(self, typename: str, id: str, message: Any)
    • Send a message to any function with the function type of the the form <namesapce>/<type> and message of type google.protobuf.Any
  • pack_and_send(self, typename: str, id: str, message)
    • The same as above, but it will pack the protobuf message in an Any
  • reply(self, message: Any)
    • Sends a message to the invoking function
  • pack_and_reply(self, message)
    • The same as above, but it will pack the protobuf message in an Any
  • send_after(self, delay: timedelta, typename: str, id: str, message: Any)
    • Sends a message after a delay
  • pack_and_send_after(self, delay: timedelta, typename: str, id: str, message)
    • The same as above, but it will pack the protobuf message in an Any
  • send_egress(self, typename, message: Any)
    • Emits a message to an egress with a typename of the form <namespace>/<name>
  • pack_and_send_egress(self, typename, message)
    • The same as above, but it will pack the protobuf message in an Any
  • __getitem__(self, name)
    • Retrieves the state registered under the name as an Any or None if no value is set
  • __delitem__(self, name)
    • Deletes the state registered under the name
  • __setitem__(self, name, value: Any)
    • Stores the value under the given name in state.