# Zoo Framework

> Zoo Framework runs long-lived background tasks **inside your own Python process** — no
> broker, no Redis/RabbitMQ, no distributed setup. It is a multi-threaded orchestration
> framework: you declare task units ("Workers"), register them, and the framework keeps
> them running — scheduled, observable, and with state on disk. Python 3.11+.

It is the in-process alternative to Celery, **not** a replacement for it. There is no HTTP
layer, no broker and no distributed scheduling.

## What this is

- **In-process scheduling** — a scheduling round runs every second and dispatches Workers
  to a thread, or to a bounded thread pool.
- **Two scheduling modes** — `thread` (one thread per dispatch) and `thread_pool` (bounded
  pool), selected by `worker:mode`.
- **Loop and single-shot workers** — declared with `is_loop`; the wait between executions is
  a fixed `delay_time`.
- **Coroutine workers** — `AsyncWorker` subclasses execute on the scheduler path, each run in
  its own event loop.
- **Event pipeline** — per-channel FIFOs, priority ordering with an age bonus so low-priority
  events do not starve, retry counts, and dead-letter records.
- **State persistence** — periodic and on-shutdown saves, written to a temporary file and
  `os.replace`d atomically, with up to 5 rolling backups.
- **In-flight tracking and timeouts** — the framework observes how long a Worker has been
  running and stops dispatching it past the timeout. It does **not** force-kill a running
  Worker.
- **Worker registration** — by class, by instance, or by factory, with metadata, tags and
  priority.
- **Nothing to run alongside it** — no broker, no cron daemon, no separate worker process.
  Install the package and embed it; there is no second service to deploy.

## When to use it

Use Zoo Framework when the work is **long-lived, in-process, and not worth a broker**:

- Background tasks that must run inside the service that owns them.
- Scheduled polling without a cron daemon — a fixed interval is enough.
- Work that needs retries, priority ordering and a dead-letter record but not a message queue.
- State that must survive a restart (atomic save + rolling backups) without a database.
- Code generated by an AI agent: the extension surface is narrow and failures are loud.

## When not to use it

Read this as a scope statement. These are not gaps to be worked around — they are things Zoo
Framework deliberately declines, and each has a better tool:

- **Work that must cross machines** → use **Celery**. It is the right answer the moment a
  task leaves the process; there is no distributed scheduling here.
- **Cron expressions** → use **APScheduler**. Zoo Framework supports a fixed `delay_time`
  interval only; cron expressions are not implemented.
- **Multi-process execution** → **not implemented** in Zoo Framework. The mode constants are
  placeholders and requesting one is rejected with
  `dispatch mode 'process' is not implemented; implemented modes are ['thread', 'thread_pool']`.
- **Health monitoring metrics** → the metric pipeline is not yet wired:
  `get_health_report()` always reports `execute_count: 0`. Do not rely on it as an alerting
  source.

## Core concepts

The framework is named after a zoo, but **the metaphor only affects naming, not semantics**.
If a name below is unclear, read the functional name in brackets and ignore the animal:

- **Worker** [`BaseWorker` subclass] — a task execution unit. Subclass it and implement
  `_execute()`.
- **Master** [`Master`] — the lifecycle entry point: load config, register Workers, start
  scheduling, shut down.
- **Waiter** [`core/waiter/`] — the scheduler. `worker:mode` picks the model
  (`thread` / `thread_pool`); `worker:runPolicy` only sets the pool's backpressure.
- **Cage** [`ScopedContainer`] — a scoped registry holding shared instances per scope
  (process / session / prototype). Instances are declared, then resolved.
- **Event** [`EventNode` / `EventChannel`] — an inter-worker message carrying a channel,
  a priority and a retry count.
- **FIFO** [`EventFIFO`] — one independent queue per channel.
- **Reactor** [`EventReactor`] — an event responder, registered with
  `@event(topic, channel=...)`.
- **StateMachine** [`StateMachineManager`] — read/write state by "scope + key path", with
  observers and persistence.

## Minimal runnable example

The same code is checked in as `example/minimal.py`; it runs without a config file.

```python
from zoo_framework.core import Master
from zoo_framework.workers import BaseWorker


class MyWorker(BaseWorker):
    def __init__(self):
        super().__init__(
            {
                "is_loop": True,      # keep running across scheduling rounds
                "delay_time": 1.0,    # seconds to wait after each execution
                "name": "MyWorker",
            }
        )
        self.counter = 0

    def _execute(self):
        self.counter += 1
        print(f"Hello from MyWorker! Count: {self.counter}")


if __name__ == "__main__":
    master = Master()
    master.register_worker("MyWorker", MyWorker)  # register the CLASS, not an instance
    master.run()
```

Install with `pip install zoo-framework` (Python 3.11+). To scaffold a project with a config
file and a demo Worker already registered, use the CLI: `zfc --create myapp`.

**A Worker must be registered as a class.** `WorkerRegistry` validates with `issubclass`, so a
function or an instance is rejected with `TypeError: issubclass() arg 1 must be a class`.
Use `register_instance` / `register_factory` for those two forms.

## Links

- [Documentation site](https://yearsalso.github.io/zoo-framework/)
- [Benchmark report](https://yearsalso.github.io/zoo-framework/benchmark/)
- [API reference](https://github.com/YearsAlso/zoo-framework/blob/dev/docs/api/README.md)
- [CHANGELOG](https://github.com/YearsAlso/zoo-framework/blob/dev/CHANGELOG.md)

License: Apache License 2.0. Maintained by one person; issues are answered on a best-effort
basis on the project's GitHub issue tracker.
