--- description: Follow these guidelines when editing the services applyTo: 'src/transcription/services/*.py' --- # Services ## Structure - Project core data models defined in [models](../../src/transcription/models.py) - 1 service class per data model - Only services directly interact with the database, and only through async methods - Services are completely independent of one another. Any operation that needs to use more than a single service, which is most of them, needs to have a separate orchestration function. ## Error Handling - Service-specific errors defined at the top of the respective module and inherit from `AppError` - Use a context manager for large `try/except` blocks like in [transcription](../../src/transcription/services/transcription.py) ## Checklist - [ ] Uses `ServiceBase` for common logic - [ ] CRUD methods created at the top - [ ] Session kwarg for `AsyncSession` to pass in a session object to each method - [ ] Services use `self._session_scope` in their methods to pass the session thru. - Multiple operations on the same object(s) require sharing a session between all the methods used. ## CRUD Methods - Create, read, update, and delete, created in that order - Name format `_`, for example `create_document` or `update_job` - All services must define these 4 methods first, and in that order ## Transaction Finalization When a service method accepts an optional `session` kwarg, write methods must use `self._finalize` to finalize the transaction properly according to whether or not they are sharing a session. - If `session` is `None`: the method owns the transaction and should `commit()`. - If `session` is provided: the method must **not** commit; it should `flush()` so IDs and FK values are available to the caller's transaction. - Use `refresh()` on returned ORM objects when the caller needs DB-populated values (defaults, triggers, merged state). Recommended helper behavior: - Inputs: active session object, original `session` arg (or a boolean ownership flag), and an optional list of objects to refresh. - Logic: `commit` when service-owned session, `flush` when caller-owned session, then refresh requested objects. This keeps orchestration functions atomic: they can pass one shared session across multiple services and commit exactly once at the workflow boundary. ## Workflow Transaction Boundaries For multi-step job lifecycles (for example queued transcription jobs), orchestration functions must use explicit transaction phases. Required boundary model: - **Transaction A (claim):** transition `JobStatus.QUEUED -> JobStatus.PROCESSING` and commit immediately. - Perform provider/network work **outside** database transactions. - **Transaction B (terminal success):** write transcript content and set `JobStatus.TRANSCRIBED` in the same shared-session commit. - **Transaction B (terminal failure):** write transcript error detail and set `JobStatus.FAILED` in the same shared-session commit. - **Transaction C (retry path):** write transcript error detail, increment retry count, and set `JobStatus.QUEUED` in one shared-session commit. Atomicity rules: - Never commit transcript updates separately from the paired terminal/retry job status change. - Terminal state (`TRANSCRIBED` or `FAILED`) and transcript row changes must succeed or roll back together. - Retry persistence (`QUEUED` + retry increment + error detail) must succeed or roll back together. Separation of concerns: - Worker modules should stay lightweight and delegate lifecycle transitions to service/workflow orchestration functions. - In `workflows.py`, `process_queued_job` should own one complete attempt lifecycle: `QUEUED -> PROCESSING -> TRANSCRIBED|FAILED`. - In `workflows.py`, `advance_job` should coordinate broader status progression around attempts (for example retry scheduling from `FAILED -> QUEUED`). - Services should expose session-aware write helpers (flush on caller-owned session) so orchestration controls commit boundaries. - Backoff/sleep behavior must run outside transactional scopes. # Service Composition Some operations, like uploading a picutre, require modifications to multiple tables, which can be done by composing methods from the service object into a separate function.