mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: close transactional boundary debt batch
This commit is contained in:
@@ -64,16 +64,17 @@ to make the directory tree look symmetrical.
|
||||
| `app/application/subscription/` | Subscription use cases: `write.py` owns media-to-row translation and the write port; `contract.py` owns shared metadata/media-key projection; query, mutation, deletion, identity and search stay in their single-word modules |
|
||||
| `app/application/search/` | Search state and later search-plan use cases |
|
||||
| `app/application/download/` | Download task querying/control and selection use cases; `failures.py` owns the frozen failure-cooldown write/query DTOs and persistence Port |
|
||||
| `app/application/history.py` | History use cases and persistence contracts; DownloadHistory owns frozen query/write DTOs and typed ports, while TransferHistory remains a separate unfinished migration surface |
|
||||
| `app/application/music/` | Multi-source music catalog orchestration |
|
||||
| `app/application/chain/` | Injectable Chain runtime capabilities: `context.py` owns the runtime dependency aggregate, `data.py` owns named persistence ports, and `events.py` owns durable event write contracts plus replayable payload conversion |
|
||||
| `app/application/agentdata.py` | Named Agent data ports; canonical Agent consumers use `get_agent_*_port()` and do not alias legacy proxies to Oper classes |
|
||||
| `app/application/outbox.py` | Durable intent and Outbox repository/dispatcher contracts for post-commit side effects |
|
||||
| `app/application/outbox.py` | Durable intent, transaction-only stager, short-transaction dispatch store, claim fencing and structured post-commit result contracts |
|
||||
| `app/application/transfer/` | Durable transfer use cases: `workflow.py` owns admission/planning/queue behavior; `execution.py` owns stable operation identity, step/checkpoint state, retry/manual-review commands and terminal-settlement DTOs |
|
||||
| `app/application/plugin/` | Plugin market catalog, installation command, installed-plugin identity contract and startup migration, runtime port, folder operations and dynamic-route use cases; filenames remain single words (`catalog.py`, `identity.py`, `migration.py`, `install.py`, `runtime.py`, `folders.py`, `routes.py`) |
|
||||
| `app/application/server/` | MoviePilot Server reporting and sharing use cases; local data readers and transport callbacks are injected by startup |
|
||||
| `app/application/site/` | Configured site catalog, authentication level and index-resource capability; the generated extension and its data bundle stay together here |
|
||||
| `app/application/messaging/` | Message rendering/routing, interactions and the Agent-to-message bridge: `ingress.py` owns the single channel-to-host loopback boundary; `interaction.py` shared interaction contracts and view helpers; `router.py` unified interaction priority and callback dispatch; `site.py`/`subscribe.py`/`skill.py` per-command sessions, input parsing and views; `media.py` media interaction state while the business workflow stays in `MediaInteractionChain`; `plugin.py` plugin input capture and plugin button callbacks; `agent.py` agent choice state, callback protocol and WebAgent bridge; `message.py` notification rendering, templates and queue. Not a public SDK recommended for direct plugin use |
|
||||
| `app/application/security/` | Authentication, authorization, cookies, passkeys, OTP/two-factor, path/URL safety, SSRF and signing policy |
|
||||
| `app/application/security/` | Authentication, authorization, frozen user/auth projections, atomic user aggregate commands, per-user configuration publication, cookies, passkeys, OTP/two-factor, path/URL safety, SSRF and signing policy |
|
||||
|
||||
Application services may use domain rules and runtime contracts. They own the
|
||||
persistence Protocol needed by a use case, but must not import `app.db`,
|
||||
@@ -457,10 +458,14 @@ SQLAlchemy models stay under `app/db/models/`; the data access classes live in
|
||||
carries the role. Two verified aggregation exceptions exist: the site family
|
||||
(`Passkey`, `SiteIcon`, `SiteStatistic`, `SiteUserData`) is consolidated in
|
||||
`oper/site.py`, and `AgentTaskRun` lives in `oper/agenttask.py`. DB adapters use
|
||||
Oper classes instead of issuing SQLAlchemy queries directly. Application and
|
||||
Chain code reaches persistence through named Ports/Protocols; concrete DB adapters
|
||||
are the layer that adapts those Ports to Oper classes. Every schema change
|
||||
requires an Alembic migration under `database/versions/`.
|
||||
Oper classes for ordinary entity access. Adapter-owned cross-row locks and
|
||||
compare-and-set transitions may issue focused SQLAlchemy statements when the
|
||||
atomic persistence invariant cannot be expressed by an entity Oper; those
|
||||
statements stay private to the adapter and require concurrency tests.
|
||||
Application and Chain code reaches persistence through named Ports/Protocols;
|
||||
concrete DB adapters are the only layer that adapts those Ports to Oper/Session
|
||||
mechanics. Every schema change requires an Alembic migration under
|
||||
`database/versions/`.
|
||||
|
||||
Oper classes take and return persistence values, not domain objects. Translating
|
||||
`MediaInfo` / `MetaBase` into a row is business logic and belongs in
|
||||
@@ -477,17 +482,40 @@ path cannot forget them. Identity representation rules themselves
|
||||
alongside the two identity mixins; `app/domain/media.py` keeps only source
|
||||
policy. `app/db` therefore has no dependency on `app/domain`.
|
||||
|
||||
User identity is a single aggregate boundary. `app/application/security/user.py`
|
||||
owns frozen user/auth snapshots and the atomic create/update/delete command;
|
||||
`app/db/adapters/user.py` binds each mutation to one request UoW and locks the
|
||||
active-superuser set before a destructive change. The database enforces unique
|
||||
user names and cascades rename/delete to `UserConfig` and delete to `PassKey`.
|
||||
The configured user-configuration repository publishes its in-memory snapshot
|
||||
only after the user transaction commits, and reloads from the database if
|
||||
publication fails.
|
||||
|
||||
Download history is the verified half of the History migration:
|
||||
`app/application/history.py` owns frozen `DownloadHistorySnapshot` /
|
||||
`DownloadFileSnapshot` values and typed query/write ports;
|
||||
`app/db/adapters/history/download.py` performs projection and mutations in short
|
||||
Session scopes. TransferHistory still uses its existing ports, so this must not
|
||||
be described as completion of the entire History boundary.
|
||||
|
||||
Durable post-commit side effects have a separate boundary:
|
||||
|
||||
- `app/application/outbox.py` owns the Outbox intent, repository and dispatcher
|
||||
contracts. An Application command stages the business mutation and its durable
|
||||
intent in the same transaction.
|
||||
- `app/db/adapters/outbox.py` implements the persistence port with SQLAlchemy;
|
||||
- `app/application/outbox.py` separates `OutboxStager`, which only stages in the
|
||||
caller's business transaction, from `OutboxDispatchStore`, whose claim,
|
||||
complete and retry operations own independent short transactions.
|
||||
- `app/db/adapters/outbox.py` implements both roles with SQLAlchemy;
|
||||
`app/startup/composition/subscription.py` and the other composition modules
|
||||
provide the concrete repository, UoW and handlers.
|
||||
- The dispatcher claims an intent with a lease, executes the topic handler, and
|
||||
records retry/dead-letter state. Handlers must be idempotent and must not rely
|
||||
on a live request object.
|
||||
inject the stager, dispatch-store factory, UoW and handlers.
|
||||
- Immediate request-thread delivery and the dispatcher both claim before
|
||||
calling a handler. Lease acquisition is atomic, while complete/retry is fenced
|
||||
by the claimed attempt so an expired owner cannot settle a newer claim.
|
||||
- `PostCommitResult` distinguishes the committed business value from completed
|
||||
and pending effects. This does not provide exactly-once delivery: a process may
|
||||
stop after an external sink succeeds but before complete is persisted. Outbox
|
||||
delivery is therefore at-least-once. Event payloads and the host correlation
|
||||
context carry the stable event key, and consumers that support deduplication
|
||||
should use it. Legacy notification plugins retain their existing method
|
||||
signature, so the host must not claim provider-level exactly-once delivery.
|
||||
- Terminal history is part of the shared data-maintenance policy and is cleaned
|
||||
in bounded daily batches only when that policy is enabled. Completed intents
|
||||
default to 30-day retention and dead letters to 90 days; both values are
|
||||
@@ -546,6 +574,10 @@ expired claimed task remains exclusively owned by fenced recovery APIs.
|
||||
- Startup registers concrete cache factories before decorated business modules
|
||||
are imported. Cache contracts remain in `app/runtime/cache.py`; Redis/file
|
||||
implementations remain in `app/adapters/cache/backends.py`.
|
||||
- Security-sensitive one-shot state uses the strict `AtomicCacheBackend`
|
||||
`store/consume` contract. Memory and Redis implement atomic consume; startup
|
||||
injects that capability through `PasskeyChallengeCache`, so Passkey
|
||||
Application code never identifies or imports the Redis implementation.
|
||||
- `app/runtime/log.py` is a dependency leaf with no `app.*` imports. Foundation
|
||||
emits no runtime logs; upper-layer owners decide whether failures are
|
||||
operationally relevant.
|
||||
@@ -573,6 +605,9 @@ expired claimed task remains exclusively owned by fenced recovery APIs.
|
||||
may use `app.core`, `app.helper`, `app.utils` or `app.log`.
|
||||
- New plugins use `app.sdk`. In DEBUG mode, a legacy plugin import remains
|
||||
functional and emits one actionable warning per plugin and legacy module.
|
||||
- Plugin compatibility changes belong only in curated SDK/Legacy exports or the
|
||||
exact Compat manifest. `app/plugins/**` contains runtime plugin copies and is
|
||||
excluded from host refactors, dependency baselines and ownership migrations.
|
||||
- Delayed imports are not accepted as a way to hide dependency cycles.
|
||||
|
||||
### Dependency facts and semantic policy
|
||||
@@ -677,8 +712,13 @@ driven workflow registration.
|
||||
| `app/application/download/failures.py` | Frozen download-failure cooldown write/query DTOs and Chain persistence Port |
|
||||
| `app/db/adapters/download.py` | Short-session download-failure snapshot and mutation adapter |
|
||||
| `app/db/adapters/mediaserver.py` | Per-operation media-server cache query/upsert/cleanup transaction adapter |
|
||||
| `app/application/outbox.py` | Durable intent, topic handler and Outbox repository contracts |
|
||||
| `app/db/adapters/outbox.py` | SQLAlchemy Outbox persistence, claim/lease and retry state adapter |
|
||||
| `app/application/history.py` | History use cases; frozen DownloadHistory DTOs and typed query/write ports, with TransferHistory migration still pending |
|
||||
| `app/db/adapters/history/download.py` | DownloadHistory short-session snapshot, query and mutation adapter |
|
||||
| `app/application/security/user.py` | Frozen user/auth projections and atomic user aggregate service contracts |
|
||||
| `app/db/adapters/user.py` | User projection plus request-UoW mutation adapter |
|
||||
| `app/db/adapters/configuration.py` | Commit-after UserConfig snapshot publication and fact-source reload adapter |
|
||||
| `app/application/outbox.py` | Durable intent, stager/store, claim fencing, topic handler and structured post-commit contracts |
|
||||
| `app/db/adapters/outbox.py` | SQLAlchemy Outbox transaction-only stagers and short-transaction claim/settlement stores |
|
||||
| `app/application/chain/events.py` | Chain durable-event write port, settlement projection and replayable payload conversion |
|
||||
| `app/application/transfer/workflow.py` | Transfer task, durable admission, versioned planning input/checkpoint contracts and queue use case |
|
||||
| `app/db/adapters/transfer/admission.py` | SQLAlchemy admission/checkpoint persistence, CAS state transition and detached snapshot adapter |
|
||||
@@ -691,6 +731,8 @@ driven workflow registration.
|
||||
| `app/startup/initializers/` | Domain-scoped initialization and shutdown hooks |
|
||||
| `app/chain/agent.py` | `AgentChain(ChainBase)`: the chain-layer entry for Agent sessions; Agent runtime stays in `app/agent/` |
|
||||
| `app/runtime/config.py` | `ConfigModel`, `Settings` and deployment configuration |
|
||||
| `app/runtime/cache.py` | Cache contracts and memory policy, including strict atomic store/consume for one-shot security state |
|
||||
| `app/application/security/passkey.py` | Injected Passkey challenge cache port and one-shot challenge issue/consume policy |
|
||||
| `app/runtime/tasks.py` | TaskRegistry owner, cancellation and bounded shutdown waiting |
|
||||
| `app/runtime/execution.py` | Shared execution/thread-boundary helpers and context propagation |
|
||||
| `app/runtime/correlation.py` | Correlation ID context and propagation boundary |
|
||||
@@ -751,4 +793,4 @@ imports, entrypoint (`api`/`agent`/`monitor`/`workflow`/`doctor`) imports of
|
||||
modules only through `run_module` dispatch), and downloader SDK
|
||||
(`qbittorrentapi`, `transmission_rpc`) imports inside `app/chain`.
|
||||
|
||||
*Last Updated: 2026-08-27*
|
||||
*Last Updated: 2026-08-28*
|
||||
|
||||
@@ -130,18 +130,45 @@ adapters; it does not retain reusable repository implementations.
|
||||
`AgentChatService.delete()`, and `DeletePluginDataCommand`: bind the repository
|
||||
and UoW to one request/operation Session. Legacy plugin-facing Oper methods may
|
||||
remain temporarily, but a new endpoint or startup workflow must call `stage_*`.
|
||||
- User create/update/delete is an aggregate command owned by
|
||||
`app/application/security/user.py`. It uses one request AsyncSession/UoW, locks
|
||||
active superusers before destructive changes, and rejects removal of the last
|
||||
enabled superuser. Query ports return frozen user/auth snapshots rather than
|
||||
ORM rows.
|
||||
- The database is the final user-identity guard: `user.name` is unique;
|
||||
`UserConfig.username` cascades on user rename/delete; `PassKey.user_id`
|
||||
cascades on user delete; `(UserConfig.username, UserConfig.key)` is unique and
|
||||
non-null. A schema change to any of these constraints requires a replay-safe
|
||||
migration that repairs legacy duplicates/orphans before creating constraints.
|
||||
- DownloadHistory is projected into frozen DTOs within the adapter Session.
|
||||
Its typed query/write port and delete mutation use short Session/UoW scopes.
|
||||
TransferHistory has not completed the same migration and must be tracked
|
||||
separately rather than treating the whole History area as typed.
|
||||
|
||||
### Durable post-commit side effects
|
||||
|
||||
Business mutations that must survive process interruption stage their durable
|
||||
intent through `app/application/outbox.py` in the same Session/UoW as the
|
||||
business row. `app/db/adapters/outbox.py` is the SQLAlchemy implementation;
|
||||
startup composition supplies the repository, transaction scope and topic
|
||||
handlers.
|
||||
intent through `OutboxStager` in the same Session/UoW as the business row.
|
||||
`OutboxDispatchStore` owns separate short transactions for claim, complete and
|
||||
retry; a business Session must never call those self-committing operations.
|
||||
`app/db/adapters/outbox.py` implements both roles, and startup composition
|
||||
supplies the stager, store factory, transaction scope and topic handlers.
|
||||
|
||||
The dispatcher claims an intent with a lease, executes an idempotent handler,
|
||||
and records bounded retries or dead-letter state. The shared data-maintenance
|
||||
policy controls bounded terminal-history cleanup, with user-configurable 30-day
|
||||
Immediate delivery and the dispatcher both claim before execution. Claim is
|
||||
atomic and complete/retry is fenced by the claimed attempt, so an expired owner
|
||||
cannot settle a newer lease. `PostCommitResult` separately reports the committed
|
||||
business value plus completed and pending effects; a post-commit failure cannot
|
||||
be represented as a rollback of already committed business data.
|
||||
|
||||
This boundary is at-least-once, not exactly-once. If an external sink succeeds
|
||||
and the process stops before complete is persisted, the intent can be replayed.
|
||||
Event payloads and the host correlation context therefore carry the stable
|
||||
event key, and consumers that support deduplication should use it. Legacy
|
||||
notification plugins retain their existing method signature and remain an
|
||||
at-least-once boundary where duplicate provider delivery is possible. The
|
||||
dispatcher records bounded retries or dead-letter state.
|
||||
The shared data-maintenance policy controls bounded terminal-history cleanup,
|
||||
with user-configurable 30-day
|
||||
completed and 90-day dead-letter defaults; `0` disables either cleanup. It must
|
||||
not delete pending or leased processing rows. The `app/runtime/tasks.py`
|
||||
TaskRegistry is only the owner for in-process work and bounded shutdown waiting;
|
||||
@@ -237,6 +264,15 @@ configuration.set(username="alice", key="notification_enabled", value=True)
|
||||
The no-Session `UserConfigOper()` form is legacy plugin ABI only and must not be
|
||||
copied into host code.
|
||||
|
||||
`TransactionalUserConfigurationRepository` stages a set in a short transaction
|
||||
and publishes the process snapshot only after commit. User rename/delete is
|
||||
first completed by the user aggregate transaction through database cascades;
|
||||
post-commit publication then acquires the write lock and reloads the database
|
||||
fact source so concurrent set/rename/delete operations converge on committed
|
||||
state. If publication fails, the repository reloads that fact source instead of
|
||||
rolling back or hiding the already committed user mutation. Reads and published
|
||||
JSON values are copied so callers cannot mutate shared cache state.
|
||||
|
||||
---
|
||||
|
||||
## Settings / Environment Configuration
|
||||
@@ -280,12 +316,19 @@ def get_movie_detail(tmdb_id: int) -> dict:
|
||||
|
||||
When `REDIS_HOST` is configured, `app/modules/redis/` provides a distributed cache backend. Prefer `FileCache` for single-node deployments.
|
||||
|
||||
Security-sensitive one-shot state uses `AtomicCacheBackend`, not a concrete
|
||||
Redis helper. Its strict `store()` surfaces backend write failures and
|
||||
`consume()` atomically returns-and-removes a value. Both Memory and Redis
|
||||
backends implement this contract; Passkey receives the capability from startup
|
||||
through `PasskeyChallengeCache`, so an authentication or registration challenge
|
||||
can be accepted only once without Application knowing the configured backend.
|
||||
|
||||
---
|
||||
|
||||
## Data Lifecycle Rules
|
||||
|
||||
- **TransferHistory:** Records are inserted after every successful file transfer. Do not delete records without user confirmation.
|
||||
- **DownloadHistory:** Records are inserted when a download task is added. Linked `DownloadFiles` records track individual files within a torrent.
|
||||
- **DownloadHistory:** Records are inserted when a download task is added. Linked `DownloadFiles` records track individual files within a torrent. Host query/write callers use frozen DTOs and the typed DownloadHistory port; ORM rows remain inside the adapter Session.
|
||||
- **SystemConfig:** Values may be read and written freely at runtime. Changes to watched config keys trigger `on_config_changed()` on registered classes via `ConfigReloadMixin`.
|
||||
- **MediaServerItem:** This is a cache of the remote media server library. It is refreshed on media server sync events and can be safely cleared and rebuilt.
|
||||
|
||||
@@ -297,4 +340,4 @@ When `REDIS_HOST` is configured, `app/modules/redis/` provides a distributed cac
|
||||
- `settings.API_TOKEN` and other secret fields must not be included in log output or API responses.
|
||||
- The `config list --show-secrets` flag exists specifically to gate secret visibility in the CLI.
|
||||
|
||||
*Last Updated: 2026-08-27*
|
||||
*Last Updated: 2026-08-28*
|
||||
|
||||
Reference in New Issue
Block a user