codecov[bot]: # [Codecov](https://codecov.io/gh/python-trio/trio/pull/576?src=pr&el=h1) Report
> Merging [#576](https://codecov.io/gh/python-trio/trio/pull/576?src=pr&el=desc) into [master](https://codecov.io/gh/python-trio/trio/commit/fd0d8770a120a09a506ccd8a3f9969b26a22f38e?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/python-trio/trio/pull/576/graphs/tree.svg?token=Lhvc0OnSCw&src=pr&height=150&width=650)](https://codecov.io/gh/python-trio/trio/pull/576?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #576      +/-   ##
==========================================
+ Coverage   99.27%   99.27%   +<.01%     
==========================================
  Files          89       89              
  Lines       10628    10640      +12     
  Branches      747      747              
==========================================
+ Hits        10551    10563      +12     
  Misses         59       59              
  Partials       18       18
```


| [Impacted Files](https://codecov.io/gh/python-trio/trio/pull/576?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [trio/\_sync.py](https://codecov.io/gh/python-trio/trio/pull/576/diff?src=pr&el=tree#diff-dHJpby9fc3luYy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [trio/tests/test\_sync.py](https://codecov.io/gh/python-trio/trio/pull/576/diff?src=pr&el=tree#diff-dHJpby90ZXN0cy90ZXN0X3N5bmMucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-trio/trio/pull/576?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-trio/trio/pull/576?src=pr&el=footer). Last update [fd0d877...26d4073](https://codecov.io/gh/python-trio/trio/pull/576?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/newsfragments/548.bugfix.rst b/newsfragments/548.bugfix.rst
new file mode 100644
index 00000000..90c549af
--- /dev/null
+++ b/newsfragments/548.bugfix.rst
@@ -0,0 +1,1 @@
+Fix a memory leak in :class:`trio.CapacityLimiter`, that could occurr when ``acquire`` or ``acquire_on_behalf_of`` was cancelled.
diff --git a/trio/_sync.py b/trio/_sync.py
index fe806dd3..a99f75a3 100644
--- a/trio/_sync.py
+++ b/trio/_sync.py
@@ -288,7 +288,11 @@ class CapacityLimiter:
         except _core.WouldBlock:
             task = _core.current_task()
             self._pending_borrowers[task] = borrower
-            await self._lot.park()
+            try:
+                await self._lot.park()
+            except _core.Cancelled:
+                self._pending_borrowers.pop(task)
+                raise
         except:
             await _core.cancel_shielded_checkpoint()
             raise

Check CapacityLimiter's handling of _pending_borrowers
I was just reading the code for an unrelated reason, and realized that this code looks very suspicious:

https://github.com/python-trio/trio/blob/65729b121c55ff0aa1f5d43bd9cdcd6a81e54f0c/trio/_sync.py#L286-L296

Specifically, if the call to `await self._lot.park()` gets cancelled, it looks like we don't clear `self._pending_borrowers[task]`, which could be a memory leak (since it pins task objects in memory).
diff --git a/trio/tests/test_sync.py b/trio/tests/test_sync.py
index bded1b05..64aa3235 100644
--- a/trio/tests/test_sync.py
+++ b/trio/tests/test_sync.py
@@ -147,6 +147,21 @@ async def test_CapacityLimiter_change_total_tokens():
         assert c.statistics().tasks_waiting == 0
 
 
+# regression test for issue #548
+async def test_CapacityLimiter_memleak_548():
+    limiter = CapacityLimiter(total_tokens=1)
+    await limiter.acquire()
+
+    async with _core.open_nursery() as n:
+        n.start_soon(limiter.acquire)
+        await wait_all_tasks_blocked()  # give it a chance to run the task
+        n.cancel_scope.cancel()
+
+    # if this is 1, the acquire call (despite being killed) is still there in the task, and will
+    # leak memory all the while the limiter is active
+    assert len(limiter._pending_borrowers) == 0
+
+
 async def test_Semaphore():
     with pytest.raises(TypeError):
         Semaphore(1.0)

pip install -e .[dev]
parse_log_pytest
requirements.txt
apt-get update apt-get install -y gcc
3.9
test-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
asttokens==3.0.0
async-generator==1.10
attrs==25.3.0
cffi==1.17.1
coverage==7.8.0
cryptography==44.0.2
decorator==5.2.1
exceptiongroup==1.2.2
executing==2.2.0
idna==3.10
iniconfig==2.1.0
ipython==8.18.1
jedi==0.19.2
matplotlib-inline==0.1.7
outcome==1.3.0.post0
packaging==24.2
parso==0.8.4
pexpect==4.9.0
pluggy==1.5.0
prompt_toolkit==3.0.50
ptyprocess==0.7.0
pure_eval==0.2.3
pycparser==2.22
Pygments==2.19.1
pyOpenSSL==25.0.0
pytest==8.3.5
pytest-cov==6.0.0
pytest-faulthandler==2.0.1
sortedcontainers==2.4.0
stack-data==0.6.3
tomli==2.2.1
traitlets==5.14.3
-e git+https://github.com/python-trio/trio.git@fd0d8770a120a09a506ccd8a3f9969b26a22f38e#egg=trio
trustme==1.2.1
typing_extensions==4.13.0
wcwidth==0.2.13

name: trio
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - asttokens==3.0.0
      - async-generator==1.10
      - attrs==25.3.0
      - cffi==1.17.1
      - coverage==7.8.0
      - cryptography==44.0.2
      - decorator==5.2.1
      - exceptiongroup==1.2.2
      - executing==2.2.0
      - idna==3.10
      - iniconfig==2.1.0
      - ipython==8.18.1
      - jedi==0.19.2
      - matplotlib-inline==0.1.7
      - outcome==1.3.0.post0
      - packaging==24.2
      - parso==0.8.4
      - pexpect==4.9.0
      - pluggy==1.5.0
      - prompt-toolkit==3.0.50
      - ptyprocess==0.7.0
      - pure-eval==0.2.3
      - pycparser==2.22
      - pygments==2.19.1
      - pyopenssl==25.0.0
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytest-faulthandler==2.0.1
      - sortedcontainers==2.4.0
      - stack-data==0.6.3
      - tomli==2.2.1
      - traitlets==5.14.3
      - trio==0.5.0+dev
      - trustme==1.2.1
      - typing-extensions==4.13.0
      - wcwidth==0.2.13
prefix: /opt/conda/envs/trio

trio/tests/test_sync.py::test_CapacityLimiter_memleak_548
trio/tests/test_sync.py::test_Event trio/tests/test_sync.py::test_CapacityLimiter trio/tests/test_sync.py::test_CapacityLimiter_change_total_tokens trio/tests/test_sync.py::test_Semaphore trio/tests/test_sync.py::test_Semaphore_bounded trio/tests/test_sync.py::test_Lock_and_StrictFIFOLock[Lock] trio/tests/test_sync.py::test_Lock_and_StrictFIFOLock[StrictFIFOLock] trio/tests/test_sync.py::test_Condition trio/tests/test_sync.py::test_Queue trio/tests/test_sync.py::test_553 trio/tests/test_sync.py::test_Queue_iter trio/tests/test_sync.py::test_Queue_statistics trio/tests/test_sync.py::test_Queue_fairness trio/tests/test_sync.py::test_Queue_unbuffered trio/tests/test_sync.py::test_generic_lock_exclusion[CapacityLimiter(1)] trio/tests/test_sync.py::test_generic_lock_exclusion[Semaphore(1)] trio/tests/test_sync.py::test_generic_lock_exclusion[Lock] trio/tests/test_sync.py::test_generic_lock_exclusion[StrictFIFOLock] trio/tests/test_sync.py::test_generic_lock_exclusion[QueueLock1(10)] trio/tests/test_sync.py::test_generic_lock_exclusion[QueueLock1(1)] trio/tests/test_sync.py::test_generic_lock_exclusion[QueueLock2] trio/tests/test_sync.py::test_generic_lock_exclusion[QueueLock3] trio/tests/test_sync.py::test_generic_lock_fifo_fairness[CapacityLimiter(1)] trio/tests/test_sync.py::test_generic_lock_fifo_fairness[Semaphore(1)] trio/tests/test_sync.py::test_generic_lock_fifo_fairness[Lock] trio/tests/test_sync.py::test_generic_lock_fifo_fairness[StrictFIFOLock] trio/tests/test_sync.py::test_generic_lock_fifo_fairness[QueueLock1(10)] trio/tests/test_sync.py::test_generic_lock_fifo_fairness[QueueLock1(1)] trio/tests/test_sync.py::test_generic_lock_fifo_fairness[QueueLock2] trio/tests/test_sync.py::test_generic_lock_fifo_fairness[QueueLock3] trio/tests/test_sync.py::test_generic_lock_acquire_nowait_blocks_acquire[CapacityLimiter(1)] trio/tests/test_sync.py::test_generic_lock_acquire_nowait_blocks_acquire[Semaphore(1)] trio/tests/test_sync.py::test_generic_lock_acquire_nowait_blocks_acquire[Lock] trio/tests/test_sync.py::test_generic_lock_acquire_nowait_blocks_acquire[StrictFIFOLock] trio/tests/test_sync.py::test_generic_lock_acquire_nowait_blocks_acquire[QueueLock1(10)] trio/tests/test_sync.py::test_generic_lock_acquire_nowait_blocks_acquire[QueueLock1(1)] trio/tests/test_sync.py::test_generic_lock_acquire_nowait_blocks_acquire[QueueLock2] trio/tests/test_sync.py::test_generic_lock_acquire_nowait_blocks_acquire[QueueLock3]
trio/_sync.py newsfragments/548.bugfix.rst
trio/_sync.py newsfragments/548.bugfix.rst