Skip to content

Commit f3a28de

Browse files
authored
Merge pull request #3818 from jamshale/sonar-cloud-reports
Fix: Some asyncio task management and modernization
2 parents 67c18b6 + 5c39d71 commit f3a28de

5 files changed

Lines changed: 155 additions & 168 deletions

File tree

acapy_agent/admin/routes.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Admin server routes."""
22

3-
import asyncio
43
import re
54

65
from aiohttp import web
@@ -219,7 +218,5 @@ async def shutdown_handler(request: web.BaseRequest):
219218
220219
"""
221220
request.app._state["ready"] = False
222-
loop = asyncio.get_event_loop()
223-
asyncio.ensure_future(request.app["conductor_stop"](), loop=loop)
224-
221+
await request.app["conductor_stop"]()
225222
return web.json_response({})

acapy_agent/admin/server.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,23 @@ async def upgrade_middleware(request: web.BaseRequest, handler: Coroutine):
216216
# We need to check for completion (or fail) in another process
217217
in_progress_upgrades.set_wallet(context.profile.name)
218218
is_subwallet = context.metadata and "wallet_id" in context.metadata
219-
asyncio.create_task(
219+
220+
# Create background task and store reference to prevent garbage collection
221+
task = asyncio.create_task(
220222
check_upgrade_completion_loop(
221223
context.profile,
222224
is_subwallet,
223225
)
224226
)
227+
228+
# Store task reference on the app to prevent garbage collection
229+
if not hasattr(request.app, "_background_tasks"):
230+
request.app._background_tasks = set()
231+
request.app._background_tasks.add(task)
232+
233+
# Remove task from set when it completes to prevent memory leaks
234+
task.add_done_callback(request.app._background_tasks.discard)
235+
225236
raise web.HTTPServiceUnavailable(reason="Upgrade in progress")
226237

227238
return await handler(request)

acapy_agent/commands/start.py

Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Entrypoint."""
22

33
import asyncio
4-
import functools
54
import logging
65
import signal
76
import sys
8-
from typing import Coroutine, Sequence
7+
from typing import Sequence
98

109
from configargparse import ArgumentParser
1110

11+
from ..config.error import ArgsParseError
12+
1213
try:
1314
import uvloop
1415
except ImportError:
@@ -24,89 +25,82 @@
2425

2526

2627
async def start_app(conductor: Conductor):
27-
"""Start up."""
28+
"""Start up the application."""
2829
await conductor.setup()
2930
await conductor.start()
3031

3132

3233
async def shutdown_app(conductor: Conductor):
33-
"""Shut down."""
34+
"""Shut down the application."""
3435
LOGGER.info("Shutting down")
3536
await conductor.stop()
3637

38+
# Cancel remaining tasks
39+
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
40+
for task in tasks:
41+
task.cancel()
42+
await asyncio.gather(*tasks, return_exceptions=True)
43+
3744

3845
def init_argument_parser(parser: ArgumentParser):
3946
"""Initialize an argument parser with the module's arguments."""
4047
return arg.load_argument_groups(parser, *arg.group.get_registered(arg.CAT_START))
4148

4249

43-
def execute(argv: Sequence[str] = None):
44-
"""Entrypoint."""
50+
async def run_app(argv: Sequence[str] = None):
51+
"""Main async runner for the app."""
4552
parser = arg.create_argument_parser(prog=PROG)
4653
parser.prog += " start"
4754
get_settings = init_argument_parser(parser)
4855
args = parser.parse_args(argv)
4956
settings = get_settings(args)
5057
common_config(settings)
5158

52-
# set ledger to read only if explicitly specified
59+
# Set ledger to read-only if explicitly specified
5360
settings["ledger.read_only"] = settings.get("read_only_ledger", False)
5461

55-
# Create the Conductor instance
56-
context_builder = DefaultContextBuilder(settings)
57-
conductor = Conductor(context_builder)
58-
59-
# Run the application
6062
if uvloop:
6163
uvloop.install()
6264
LOGGER.info("uvloop installed")
63-
run_loop(start_app(conductor), shutdown_app(conductor))
64-
65-
66-
def run_loop(startup: Coroutine, shutdown: Coroutine):
67-
"""Execute the application, handling signals and ctrl-c."""
68-
69-
async def init(cleanup):
70-
"""Perform startup, terminating if an exception occurs."""
71-
try:
72-
await startup
73-
except Exception:
74-
LOGGER.exception("Exception during startup:")
75-
cleanup()
76-
77-
async def done():
78-
"""Run shutdown and clean up any outstanding tasks."""
79-
await shutdown
80-
81-
if sys.version_info.major == 3 and sys.version_info.minor > 6:
82-
all_tasks = asyncio.all_tasks()
83-
current_task = asyncio.current_task()
84-
else:
85-
all_tasks = asyncio.Task.all_tasks()
86-
current_task = asyncio.Task.current_task()
87-
88-
tasks = [task for task in all_tasks if task is not current_task]
89-
for task in tasks:
90-
task.cancel()
91-
if tasks:
92-
await asyncio.gather(*tasks, return_exceptions=True)
93-
asyncio.get_event_loop().stop()
94-
95-
loop = asyncio.get_event_loop()
96-
cleanup = functools.partial(asyncio.ensure_future, done(), loop=loop)
97-
loop.add_signal_handler(signal.SIGTERM, cleanup)
98-
asyncio.ensure_future(init(cleanup), loop=loop)
9965

66+
context_builder = DefaultContextBuilder(settings)
67+
conductor = Conductor(context_builder)
68+
69+
loop = asyncio.get_running_loop()
70+
shutdown_event = asyncio.Event()
71+
72+
def handle_signal():
73+
LOGGER.info("Received stop signal")
74+
shutdown_event.set()
75+
76+
loop.add_signal_handler(signal.SIGTERM, handle_signal)
77+
loop.add_signal_handler(signal.SIGINT, handle_signal)
78+
79+
try:
80+
await start_app(conductor)
81+
await shutdown_event.wait()
82+
finally:
83+
await shutdown_app(conductor)
84+
85+
86+
def execute(argv: Sequence[str] = None):
87+
"""Entrypoint."""
10088
try:
101-
loop.run_forever()
89+
asyncio.run(run_app(argv))
90+
except ArgsParseError as e:
91+
LOGGER.error("Argument parsing error: %s", e)
92+
raise e
10293
except KeyboardInterrupt:
103-
loop.run_until_complete(done())
94+
LOGGER.info("Interrupted by user")
95+
except Exception:
96+
LOGGER.exception("Unexpected exception during execution")
97+
sys.exit(1)
10498

10599

106100
def main():
107101
"""Execute the main line."""
108-
if __name__ == "__main__":
109-
execute()
102+
execute()
110103

111104

112-
main()
105+
if __name__ == "__main__":
106+
main()

0 commit comments

Comments
 (0)