aury-boot 0.0.28__py3-none-any.whl → 0.0.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aury/boot/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.28'
32
- __version_tuple__ = version_tuple = (0, 0, 28)
31
+ __version__ = version = '0.0.29'
32
+ __version_tuple__ = version_tuple = (0, 0, 29)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -164,15 +164,25 @@ class User(Base): ...
164
164
  - 写操作**必须**使用 `@transactional` 装饰器
165
165
  - 只读操作可以不加事务装饰器
166
166
  - 跨 Service 调用通过共享 session 实现事务共享
167
+ - **后台任务必须**使用 `@isolated_task` 装饰器
167
168
 
168
169
  ```python
169
- from aury.boot.domain.transaction import transactional
170
+ from aury.boot.domain.transaction import transactional, isolated_task
170
171
 
171
172
  class UserService(BaseService):
172
173
  @transactional
173
174
  async def create(self, data: UserCreate) -> User:
174
175
  # 自动事务管理
175
176
  return await self.repo.create(data.model_dump())
177
+
178
+
179
+ # 后台任务必须加 @isolated_task,否则事务不会提交
180
+ @isolated_task
181
+ async def background_upload(space_id: int, url: str):
182
+ async with db.session() as session:
183
+ async with transactional_context(session):
184
+ repo = SpaceRepository(session, Space)
185
+ await repo.update(...)
176
186
  ```
177
187
 
178
188
  ### Manager API 规范
@@ -396,3 +396,63 @@ DATABASE_ISOLATION_LEVEL=REPEATABLE READ
396
396
  - 大多数场景:`READ COMMITTED`(平衡性能和一致性)
397
397
  - 报表/统计查询:`REPEATABLE READ`(保证读取一致性)
398
398
  - 金融交易:`SERIALIZABLE`(最强一致性,性能较低)
399
+
400
+ ### 3.12 后台任务事务隔离(重要)
401
+
402
+ 在 `@transactional` 装饰的 Service 方法中 spawn 后台任务时,**必须**使用 `@isolated_task` 或 `isolated_context`,否则事务不会提交。
403
+
404
+ **问题背景**:
405
+ `asyncio.create_task()` 会继承父协程的 `contextvars`。如果父协程在 `@transactional` 中,子任务会继承事务深度标记,导致:
406
+ - `auto_commit` 失效
407
+ - `transactional_context` 也不会提交
408
+ - session 关闭时数据被 rollback
409
+
410
+ **解决方案 1:装饰器(推荐)**
411
+
412
+ ```python
413
+ import asyncio
414
+ from aury.boot.domain.transaction import isolated_task, transactional_context
415
+ from aury.boot.infrastructure.database import DatabaseManager
416
+
417
+ db = DatabaseManager.get_instance()
418
+
419
+
420
+ @isolated_task
421
+ async def upload_cover(space_id: int, cover_url: str):
422
+ """后台任务:上传封面。"""
423
+ async with db.session() as session:
424
+ async with transactional_context(session):
425
+ repo = SpaceRepository(session, Space)
426
+ space = await repo.get(space_id)
427
+ if space:
428
+ await repo.update(space, {{"cover": cover_url}})
429
+ # 现在会正常 commit
430
+
431
+
432
+ class SpaceService(BaseService):
433
+ @transactional
434
+ async def create(self, data: SpaceCreate) -> Space:
435
+ space = await self.repo.create(data.model_dump())
436
+
437
+ # spawn 后台任务
438
+ asyncio.create_task(upload_cover(space.id, data.cover_url))
439
+
440
+ return space
441
+ ```
442
+
443
+ **解决方案 2:上下文管理器**
444
+
445
+ ```python
446
+ from aury.boot.domain.transaction import isolated_context
447
+
448
+ async def background_job():
449
+ async with isolated_context():
450
+ async with db.session() as session:
451
+ async with transactional_context(session):
452
+ # 正常的事务处理
453
+ ...
454
+ ```
455
+
456
+ **注意事项**:
457
+ - 后台任务必须新开 session(`db.session()`),不能复用主请求的 `self.session`
458
+ - 后台任务的事务与主请求独立,主请求回滚不影响后台任务
@@ -390,11 +390,68 @@ def requires_transaction(func: Callable) -> Callable:
390
390
  return wrapper
391
391
 
392
392
 
393
+ def isolated_task[T](func: Callable[..., T]) -> Callable[..., T]:
394
+ """后台任务隔离装饰器。
395
+
396
+ 重置事务上下文,避免从父协程继承 _transaction_depth 导致 auto_commit 失效。
397
+
398
+ 问题背景:
399
+ asyncio.create_task() 会继承父协程的 contextvars。如果父协程在 @transactional 中,
400
+ _transaction_depth > 0,子任务的 auto_commit 和 transactional_context 都会认为
401
+ "在事务中" 而跳过 commit,导致 session 关闭时 rollback。
402
+
403
+ 用法:
404
+ @isolated_task
405
+ async def upload_cover(space_id: int, cover_url: str):
406
+ async with db.session() as session:
407
+ async with transactional_context(session):
408
+ repo = SpaceRepository(session, Space)
409
+ space = await repo.get(space_id)
410
+ await repo.update(space, {"cover": cover_url})
411
+ # 现在会正常 commit
412
+
413
+ # 在 Service 中 spawn
414
+ asyncio.create_task(upload_cover(space.id, url))
415
+ """
416
+ @wraps(func)
417
+ async def wrapper(*args, **kwargs):
418
+ # 重置事务深度,让当前任务成为独立的事务上下文
419
+ token = _transaction_depth.set(0)
420
+ try:
421
+ return await func(*args, **kwargs)
422
+ finally:
423
+ _transaction_depth.reset(token)
424
+
425
+ return wrapper
426
+
427
+
428
+ @asynccontextmanager
429
+ async def isolated_context() -> AsyncGenerator[None]:
430
+ """后台任务隔离上下文管理器。
431
+
432
+ 与 @isolated_task 作用相同,但用于上下文管理器形式。
433
+
434
+ 用法:
435
+ async def background_job():
436
+ async with isolated_context():
437
+ async with db.session() as session:
438
+ async with transactional_context(session):
439
+ ...
440
+ """
441
+ token = _transaction_depth.set(0)
442
+ try:
443
+ yield
444
+ finally:
445
+ _transaction_depth.reset(token)
446
+
447
+
393
448
  __all__ = [
394
449
  "TransactionManager",
395
450
  "TransactionRequiredError",
396
451
  "_transaction_depth", # 内部使用,不对外文档化
397
452
  "ensure_transaction",
453
+ "isolated_context",
454
+ "isolated_task",
398
455
  "on_commit",
399
456
  "requires_transaction",
400
457
  "transactional",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aury-boot
3
- Version: 0.0.28
3
+ Version: 0.0.29
4
4
  Summary: Aury Boot - 基于 FastAPI 生态的企业级 API 开发框架
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: alembic>=1.17.2
@@ -1,5 +1,5 @@
1
1
  aury/boot/__init__.py,sha256=pCno-EInnpIBa1OtxNYF-JWf9j95Cd2h6vmu0xqa_-4,1791
2
- aury/boot/_version.py,sha256=rsZxmANBVfE-nN63y-ZZ_71Lkm6YP4u4TgVEBJV3mNM,706
2
+ aury/boot/_version.py,sha256=p4kcB0BmpcnphBIseH7188jNCCOqFZSly7Pka9LrUDM,706
3
3
  aury/boot/application/__init__.py,sha256=0o_XmiwFCeAu06VHggS8I1e7_nSMoRq0Hcm0fYfCywU,3071
4
4
  aury/boot/application/adapter/__init__.py,sha256=e1bcSb1bxUMfofTwiCuHBZJk5-STkMCWPF2EJXHQ7UU,3976
5
5
  aury/boot/application/adapter/base.py,sha256=Ar_66fiHPDEmV-1DKnqXKwc53p3pozG31bgTJTEUriY,15763
@@ -61,7 +61,7 @@ aury/boot/commands/templates/generate/model.py.tpl,sha256=knFwMyGZ7wMpzH4_bQD_V1
61
61
  aury/boot/commands/templates/generate/repository.py.tpl,sha256=xoEg6lPAaLIRDeFy4I0FBsPPVLSy91h6xosAlaCL_mM,590
62
62
  aury/boot/commands/templates/generate/schema.py.tpl,sha256=HIaY5B0UG_S188nQLrZDEJ0q73WPdb7BmCdc0tseZA4,545
63
63
  aury/boot/commands/templates/generate/service.py.tpl,sha256=2hwQ8e4a5d_bIMx_jGDobdmKPMFLBlfQrQVQH4Ym5k4,1842
64
- aury/boot/commands/templates/project/AGENTS.md.tpl,sha256=-KrVhPqwVaf1IWRIl4L9Yb0LSRihq69ZaZ-ynnGM4W8,7882
64
+ aury/boot/commands/templates/project/AGENTS.md.tpl,sha256=SpqoXZXCyWmitBV6Mt0VvXWdPZAZXsLoU-vDEyJyQDc,8269
65
65
  aury/boot/commands/templates/project/README.md.tpl,sha256=oCeBiukk6Pa3hrCKybkfM2sIRHsPZ15nlwuFTUSFDwY,2459
66
66
  aury/boot/commands/templates/project/admin_console_init.py.tpl,sha256=K81L14thyEhRA8lFCQJVZL_NU22-sBz0xS68MJPeoCo,1541
67
67
  aury/boot/commands/templates/project/config.py.tpl,sha256=H_B05FypBJxTjb7qIL91zC1C9e37Pk7C9gO0-b3CqNs,1009
@@ -71,7 +71,7 @@ aury/boot/commands/templates/project/main.py.tpl,sha256=Q61ve3o1VkNPv8wcQK7lUosn
71
71
  aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl,sha256=8Aept3yEAe9cVdRvkddr_oEF-lr2riPXYRzBuw_6DBA,2138
72
72
  aury/boot/commands/templates/project/aury_docs/01-model.md.tpl,sha256=1mQ3hGDxqEZjev4CD5-3dzYRFVonPNcAaStI1UBEUyM,6811
73
73
  aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl,sha256=JfUVdrIgW7_J6JGCcB-_uP_x-gCtjKiewwGv4Xr44QI,7803
74
- aury/boot/commands/templates/project/aury_docs/03-service.md.tpl,sha256=Dg_8RGSeRmmyQrhhpppEoxl-6C5pNe9M2OzVOl1kjSk,13102
74
+ aury/boot/commands/templates/project/aury_docs/03-service.md.tpl,sha256=SgfPAgLVi_RbMjSAe-m49jQOIr6vfUT8VF2V1E-qa3w,15098
75
75
  aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl,sha256=ZwwKhUbLI--PEEmwnuo2fIZrhCEZagBN6fRNDTFCnNk,2891
76
76
  aury/boot/commands/templates/project/aury_docs/05-api.md.tpl,sha256=oPzda3V6ZPDDEW-5MwyzmsMRuu5mXrsRGEq3lj0M-58,2997
77
77
  aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl,sha256=Tv_Q5lsScHzvtcaFWmuQzN4YqvpcWZIdXS8jw99K29E,3340
@@ -130,7 +130,7 @@ aury/boot/domain/repository/interface.py,sha256=CmkiqVhhHPx_xcpuBCz11Vr26-govwYB
130
130
  aury/boot/domain/repository/query_builder.py,sha256=pFErMzsBql-T6gBX0S4FxIheCkNaGjpSewzcJ2DxrUU,10890
131
131
  aury/boot/domain/service/__init__.py,sha256=ZRotaBlqJXn7ebPTQjjoHtorpQREk8AgTD69UCcRd1k,118
132
132
  aury/boot/domain/service/base.py,sha256=6sN0nf8r5yUZsE6AcZOiOXFCqzb61oCxTfrWlqjIo9I,2035
133
- aury/boot/domain/transaction/__init__.py,sha256=Mk-J-5VOuuQ7NDW1DQmU-skCvcUOuEENfP_Ej3iZuJ8,13497
133
+ aury/boot/domain/transaction/__init__.py,sha256=EKnjJ235SYjMCvGIuLVlTdYRzU35RxNMejRGUExYqqE,15488
134
134
  aury/boot/infrastructure/__init__.py,sha256=ppP1-suaDICMNvBSXj_4DVSH3h0D8e0qZhtENCr16m8,3007
135
135
  aury/boot/infrastructure/cache/__init__.py,sha256=G40uCkpJ1jSs2fc_CBDem73iQQzCcp-4GG1WpDJzwaA,658
136
136
  aury/boot/infrastructure/cache/backends.py,sha256=9QMQ8G9DtZgzVXZ_Ng7n1gXRu-_OQZgw4FHPOfr1qco,13585
@@ -192,7 +192,7 @@ aury/boot/testing/client.py,sha256=KOg1EemuIVsBG68G5y0DjSxZGcIQVdWQ4ASaHE3o1R0,4
192
192
  aury/boot/testing/factory.py,sha256=8GvwX9qIDu0L65gzJMlrWB0xbmJ-7zPHuwk3eECULcg,5185
193
193
  aury/boot/toolkit/__init__.py,sha256=AcyVb9fDf3CaEmJPNkWC4iGv32qCPyk4BuFKSuNiJRQ,334
194
194
  aury/boot/toolkit/http/__init__.py,sha256=zIPmpIZ9Qbqe25VmEr7jixoY2fkRbLm7NkCB9vKpg6I,11039
195
- aury_boot-0.0.28.dist-info/METADATA,sha256=7fabDsArEBQ_L5MoamQPJa9xmdi19-CLTSGG5YgPQPw,7695
196
- aury_boot-0.0.28.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
197
- aury_boot-0.0.28.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
198
- aury_boot-0.0.28.dist-info/RECORD,,
195
+ aury_boot-0.0.29.dist-info/METADATA,sha256=EaL1z6pE7pRmQ6YSAytT91uPj34ejAyuzeTpWWpAI-g,7695
196
+ aury_boot-0.0.29.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
197
+ aury_boot-0.0.29.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
198
+ aury_boot-0.0.29.dist-info/RECORD,,