faust-streaming

repository·master·Indexed 23 days ago

https://github.com/faust-streaming/faust

A fork of the Faust project, faust-streaming is an asyncio-based Python library for stream processing. It provides a framework for building scalable event processing pipelines featuring an in-memory durable K/V store, distributed state via Faust Tables (backed by RocksDB or Aerospike), and integration with Kafka topics using records and agents.

Tokens
53.8K
Snippets
173
Records
306
Agent score
81%

What's inside faust-streaming

  1. Overview of Faust core modules

    master

    Faust is organized into several core modules that handle different aspects of stream processing:

    • faust.app: Defines the Faust application, including configuration and message production.
    • faust.cli: The command-line interface.
    • faust.exceptions: Custom exceptions.
    • faust.models: Defines how message keys and values are serialized/deserialized.
    • faust.sensors: Records statistics from a running application.
    • faust.serializers: Handles JSON serialization and encoding codecs.
    • faust.stores: Manages table storage (e.g., in-memory, RocksDB).
    • faust.streams: Implementation of Streams and Tables.
    • faust.topics: Tools for creating and managing topic descriptions.
    • faust.transport: Message transport implementations (e.g., aiokafka).
    • faust.types: Public interface for static typing.
    • faust.utils: General utilities.
    • faust.web: Web abstractions and the Faust web server.
    • faust.windows: Windowing strategies.
    • faust.worker: Deployment helpers for signal handling and graceful shutdown.
  2. What is a Faust Application?

    master

    An App is the central instance of the Faust library. It serves as the core API provider and manages the lifecycle of stream processors (agents), topics, channels, web views, and CLI commands.

    Key characteristics:

    • Thread Safety: You can share an app instance between multiple threads.
    • Multi-tenancy: It is safe to run multiple application instances within the same process.

    To create an application, you must provide a unique name (ID), a message broker URL, and optionally a storage driver for tables.

    import faust
    app = faust.App('example', broker='kafka://', store='rocksdb://')
  3. What is LiveCheck and when to use it

    master

    LiveCheck is an end-to-end testing tool designed for production or staging environments in microservice architectures or any asynchronous system (e.g., a monolith sending Celery tasks).

    It acts as a passive observer that tracks requests as they travel through your system and allows you to define contracts that must be met at every step. Because it is a passive observer, it can detect and alert when subsystems are down or when anomalies occur (e.g., checking if an account debt exceeds a threshold after a specific change).

    Key features:

    • Probabilistic Execution: Tests are executed based on a defined probability (e.g., 0.1%, 30%, or 50% of requests), allowing you to run tests on a subset of production traffic.
    • Stream Processor Integration: Every LiveCheck test case is a stream processor, meaning it can utilize Faust tables to store and manage stateful data during the test lifecycle.
  4. What is an Agent and how to create one

    master

    An agent is a distributed system processing events in a stream. Each event is a key/value pair, typically described using faust.Record for type safety and serialization.

    To create an agent, use the @app.agent decorator on an async function. The function must take a stream as its argument and iterate over it using async for.

    Partitioning Behavior:

    • By Key: Messages with the same key are always delivered to the same agent instance. This is useful for stateful processing.
    • Round-Robin: If no key is set (key=None), messages are distributed evenly across available workers. This is ideal for distributing work in a cluster.

    Fault Tolerance: If a worker fails, Kafka moves the partition to an online worker. Faust uses "standby tables" and a custom partition manager to optimize startup time and availability.

    import faust
    
    app = faust.App('example', broker='kafka://localhost:9092')
    
    @app.agent()
    async def myagent(stream):
        async for event in stream:
            ...  # process event
  5. Use polymorphic fields for runtime type resolution

    master

    Polymorphic fields allow a field to hold different types of models at runtime. To enable this, set polymorphic_fields=True on the parent model.

    Faust handles this by adding a __faust metadata field to the serialized payload, which contains the namespace (ns) of the model. During deserialization, Faust uses this metadata to reconstruct the correct specific subclass.

    Important: You must import the specific model classes before attempting to deserialize them, otherwise Faust will not find them in its internal index.

    import faust
    from typing import List
    
    class Asset(faust.Record):
        url: str
        type: str
    
    class ImageAsset(Asset):
        type = 'image'
    
    class VideoAsset(Asset):
        runtime_seconds: float
        type = 'video'
    
    class Article(faust.Record, polymorphic_fields=True):
        assets: List[Asset]
  6. Follow Faust coding style and type conventions

    master

    Faust follows strict coding standards:

    • Static Typing: Uses mypy. To keep imports lightweight, interfaces for classes are defined in faust/types/. For example, faust.App has a corresponding faust.types.app.AppT.
    • PEP-8: All code must follow PEP-8 guidelines.
    • Docstrings: Must follow PEP-257. Use a short description followed by more details in a new paragraph.
    • Line Length: A soft limit of 78 columns (hard limit of 79).
    • Import Order:
      1. Python standard library
      2. Third-party packages
      3. Other modules from the current package (Sorted by module name within sections)
    • No Wildcards: from xxx import * is prohibited.
    def method(self, arg: str) -> None:
        """Short description.
    
        More details.
        """
  7. How Services work in Faust

    master

    A Service is a component that can be started and stopped. Faust is composed of many services, including App, Stream, Agent, Table, and TableManager.

    Services are built using the mode.Service class. You can register a service class with your app using the @app.service decorator. Services can implement on_start and on_stop lifecycle methods and can run background tasks using the @Service.task decorator.

    import faust
    from mode import Service
    
    app = faust.App('service-example')
    
    @app.service
    class MyService(Service):
    
        async def on_start(self):
            print('MYSERVICE IS STARTING')
    
        async def on_stop(self):
            print('MYSERVICE IS STOPPING')
    
        @Service.task
        async def _background_task(self):
            while not self.should_stop:
                print('BACKGROUND TASK WAKE UP')
                await self.sleep(1.0)
    
    if __name__ == '__main__':
        app.main()
  8. Understand the relationship between Agents, Streams, Channels, and Topics

    master

    Faust uses a layered abstraction model to decouple message processing from the underlying transport mechanism. This allows agents to work with different types of communication backends.

    • Agent: The processing unit that iterates over streams.
    • Stream: An abstraction that iterates over channels.
    • Channel: A buffer or queue used to send and receive messages. Channels can be local (in-memory) or network-based.
    • Topic: A named channel backed by a transport (like a Kafka topic).
    • Transport: The underlying driver (e.g., aiokafka) that handles the actual network communication.

    Abstraction Hierarchy:

    • For local processing: Agent <--> Stream <--> Channel
    • For distributed processing: Agent <--> Stream <--> Topic <--> Transport <--> aiokafka

    Because Topics are highly Kafka-specific, you should subclass Channels if you need to implement different communication means like RabbitMQ, MQTT, or ZeroMQ.

  9. Use Faust Blueprints for reusable web components

    master

    Faust supports a Blueprint pattern for defining reusable web components. A web.Blueprint can contain routes and views, which are then registered to a Faust app with an optional url_prefix.

    from faust import web
    
    blueprint = web.Blueprint('user')
    
    @blueprint.route('/', name='list')
    class UserListView(web.View):
        async def get(self, request: web.Request) -> web.Response:
            return self.json({'hello': 'world'})
    
    @blueprint.route('/{username}/', name='detail')
    class UserDetailView(web.View):
        async def get(self, request: web.Request) -> web.Response:
            name = request.match_info['username']
            return self.json({'hello': name})
    
        async def post(self, request: web.Request) -> web.Response:
            ...
    
        async def delete(self, request: web.Request) -> web.Response:
            ...
    
    # Register the blueprint to an app
    blueprint.register(app, url_prefix='/users/')
  10. Use Application Signals to observe events

    master

    Faust uses an Observer design pattern via Signals. Signals allow you to react to lifecycle events or data events.

    Important Rules for Signal Handlers:

    1. Keyword Arguments: Handlers MUST always accept **kwargs to ensure backward compatibility when new arguments are added to the signal.
    2. Synchronous vs Asynchronous:
      • If the signal is synchronous (e.g., on_produce_message, on_configured), use def (not async def).
      • If the signal is asynchronous (e.g., on_partitions_revoked), use async def.
    3. Instance vs Class: Connecting to app.signal.connect applies to that specific app instance. Connecting to faust.App.signal.connect applies to all app instances.