summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Grönholm <alex.gronholm@nextday.fi>2021-01-10 23:35:21 +0200
committerAlex Grönholm <alex.gronholm@nextday.fi>2021-01-10 23:35:21 +0200
commit1137ce14e9b7c4bea9aba017654bd3b84f70ca27 (patch)
treebeb37bb92535f2e3b16e3ecc9a8c6769f3eb7c9c
parent9f7c1dc61ba3247b698ce177d1e7e7de8e76777d (diff)
downloadapscheduler-1137ce14e9b7c4bea9aba017654bd3b84f70ca27.tar.gz
Fixed memory leak when coroutine jobs raise exceptions
Fixes #406. Closes #418.
-rw-r--r--apscheduler/executors/base_py3.py2
-rw-r--r--docs/versionhistory.rst1
-rw-r--r--tests/test_executors_py35.py25
3 files changed, 27 insertions, 1 deletions
diff --git a/apscheduler/executors/base_py3.py b/apscheduler/executors/base_py3.py
index 61abd84..7111d2a 100644
--- a/apscheduler/executors/base_py3.py
+++ b/apscheduler/executors/base_py3.py
@@ -1,5 +1,6 @@
import logging
import sys
+import traceback
from datetime import datetime, timedelta
from traceback import format_tb
@@ -33,6 +34,7 @@ async def run_coroutine_job(job, jobstore_alias, run_times, logger_name):
events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time,
exception=exc, traceback=formatted_tb))
logger.exception('Job "%s" raised an exception', job)
+ traceback.clear_frames(tb)
else:
events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time,
retval=retval))
diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index 488adba..8c5d552 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -17,6 +17,7 @@ APScheduler, see the :doc:`migration section <migration>`.
* Added PySide2 support (PR by Abdulla Ibrahim)
* Fixed ``BlockingScheduler`` and ``BackgroundScheduler`` shutdown hanging after the user has
erroneously tried to start it twice
+* Fixed memory leak when coroutine jobs raise exceptions (due to reference cycles in tracebacks)
3.6.3
diff --git a/tests/test_executors_py35.py b/tests/test_executors_py35.py
index 7849eb0..ee66584 100644
--- a/tests/test_executors_py35.py
+++ b/tests/test_executors_py35.py
@@ -1,13 +1,18 @@
"""Contains test functions using Python 3.3+ syntax."""
+import gc
from asyncio import CancelledError
from datetime import datetime
+from unittest.mock import Mock, patch
import pytest
+from pytz import utc
+
from apscheduler.executors.asyncio import AsyncIOExecutor
+from apscheduler.executors.base_py3 import run_coroutine_job
from apscheduler.executors.tornado import TornadoExecutor
+from apscheduler.job import Job
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.schedulers.tornado import TornadoScheduler
-from pytz import utc
@pytest.fixture
@@ -100,3 +105,21 @@ async def test_asyncio_executor_shutdown(asyncio_scheduler, asyncio_executor):
asyncio_executor.shutdown()
with pytest.raises(CancelledError):
await futures.pop()
+
+
+@pytest.mark.asyncio
+async def test_run_job_memory_leak():
+ class FooBar:
+ pass
+
+ async def func():
+ foo = FooBar() # noqa: F841
+ raise Exception('dummy')
+
+ fake_job = Mock(Job, func=func, args=(), kwargs={}, misfire_grace_time=1)
+ with patch('logging.getLogger'):
+ for _ in range(5):
+ await run_coroutine_job(fake_job, 'foo', [datetime.now(utc)], __name__)
+
+ foos = [x for x in gc.get_objects() if type(x) is FooBar]
+ assert len(foos) == 0