Part III — Branches, sap and seeds

06 · The branches: services, API and CLI

Few thick branches grow out of the trunk, and many thin ones out of them. In Baobáxia: one big service (SaberService) and many small services around it.

Files to read

  1. services/saber_service.py — SaberService (≈ 4000 lines: read it by section; the # ==== ESCRITA: … ==== comments are the table of contents).
  2. api/v3/app.py — how the application is assembled, middlewares, error handlers.
  3. api/v3/deps.py — the global service and the moc of each request.
  4. One route from each router: saberes.py, estrutura.py, colecoes.py, sync.py, pedidos.py, historico.py.
  5. services/jobs.py, services/cache_leitura.py, infra/vigia_api.py.
  6. publica/operacoes.py + cli.py — the same operation serving the CLI and the bot.
  7. applications/ — the lenses (collection, blog, map, timeline, calendar).

The map of the API

RouterRoutesWhat for
saberes.py35CRUD, attachments, thumb, subtitles, text, preview, copy/move, protection
estrutura.py32galaxies, mucuas, territories, mocambolas, natures, tags, licenses
sync.py32local mucua, Tor, SSH keys, links, installing a galaxy, sync, discovery, local network
auth.py11login, token, sign-up, challenge, identity
colecoes.py10collections and stories, members, cover
pedidos.py8requests and routes (store-and-forward)
historico.py, recovery.py6 + 6git timeline, restoring; identity recovery
relacoes.py, xmpp.py, activitypub.py4 + 3 + 4relations; XMPP bridge; Fediverse (only with BBX_FED_AP=on)
app/<lens>/…—routes each application registers

Live documentation: http://127.0.0.1:8000/docs.

Inside SaberService

classDiagram
    class SaberService {
        -dict _repos  «uuid → SaberRepository»
        -dict _graphs
        +galaxy_uuids() list
        +repo(galaxia) SaberRepository
        +search(… actor) Page
        +get_saber(uuid, actor) dict
        +create_saber / update_saber / delete_saber
        +add_contents(uuid, files, actor)
        +content_path / thumbnail_path (actor)
        +get_collection / membros_dinamicos / add_collection_member …
        +find_mocambola(username) (repo, saber)
        +effective_role(saber) str
        +galaxias_administradas(uuid) list
        +historico_galaxia / restaurar
        -_find_saber(uuid, galaxia)
        -_descritor(…) dict
        -_pos_escrita(repo, uuid)
    }
    class CopiasService
    class PedidosService
    class Operacoes {
        +buscar / publicar / editar / deletar
    }
    class InProcessBackend
    class HttpBackend
    SaberService "1" o-- "*" SaberRepository
    SaberService ..> CopiasService
    SaberService ..> PedidosService
    Operacoes --> InProcessBackend : CLI local / bot
    Operacoes --> HttpBackend : bbx --api-url
    InProcessBackend --> SaberService

SaberService is a "god object": it concentrates a lot. It is the first candidate to split up when the team has the energy — see exercise 5.

The API must not freeze

A mucua runs on modest hardware. Lessons that are in the code:

  • Heavy work leaves the event loop. Uploads copy in blocks in the threadpool; sync, installs and requests become jobs (services/jobs.py): the route answers 202 with a job_id and the front follows GET /sync/jobs/{id}.
  • Repeated reads become one (cache_leitura.py, single-flight + a short TTL invalidated by index.geracao()).
  • The watcher (vigia_api.py) logs stuck requests and the thread stacks before the healthcheck restarts the service.
  • 503 with Retry-After when a file is being fetched from another mucua: the front waits and tries again (fetchComEspera).
sequenceDiagram
    actor P as Person
    participant A as POST /sync/instalar-galaxia
    participant J as jobs
    participant T as thread
    P->>A: url, somente_leitura
    A->>J: criar("instalar-galaxia")
    A->>T: executar_em_thread(job)
    A-->>P: 202 {id, acompanhar}
    T->>T: clone → reindex → mirror identity
    T->>J: progress("cloning…", "reindexing…")
    P->>J: GET /sync/jobs/{id} (until done)

The galaxy as a .zip uses the same mould: POST /galaxias/{uuid}/exportar is a job that writes the zip (and its result carries the download link), and POST /sync/importar-galaxia receives the file in the body and installs it in a job — the same _ativar_galaxia_instalada (reindex, mirror identity) as an install by URL. From the CLI: bbx exportar-galaxia and bbx importar-galaxia.

CLI and bot: a single layer of operations

publica/operacoes.py implements search/publish/edit/delete once. The CLI and the XMPP bot call the same Operacoes, with two "backends": in-process (talks directly to SaberService) or HTTP (bbx --api-url, talks to a remote mucua).

Practice

  1. Write a new route GET /saberes/{uuid}/resumo that returns the title, nature and number of attachments, respecting read permissions. Use get_saber with actor. Test it by calling the function directly, as the tests do.
  2. In test_jobs_e_instalar_async.py, why does the job keep the real error from git clone?
  3. Run bbx buscar pesca --json and then bbx buscar pesca --api-url http://127.0.0.1:8000/api/v3. Where do the two paths split in the code?
  4. Open a zip made by bbx exportar-galaxia. Why does it carry a git bundle and loose attachments, and not the whole .git folder? (Hint: what lives in .git/config?) And what does ler_manifesto refuse?
  5. Propose how to split SaberService into three (reading, writing, collections) without breaking the routes. Which private methods would be shared?

To talk about

  • Why shouldn't a slow operation hold on to the HTTP request, when the person on the other end has a poor internet connection?

vedi questo capitolo come sapere