Skip to content

Commit 0f577e2

Browse files
authored
Merge pull request #939 from cderici/green-ci-cleanup
#939 #### Description There's a couple of known intermittent failures in CI, some excessive warning logs and asyncio outputs. This PR attempts to clear up some of these issues for the new release. Some major ones of the known CI failures are as follows (will be updated on the go) in the QA steps below. All the tests should be green before this lands. #### QA Steps ``` tox -e integration --tests/integration/test_model.py::test_add_and_list_storage ``` ``` tox -e integration -- tests/integration/test_controller.py::test_destroy_model_by_name ``` ``` tox -e integration -- tests/integration/test_controller.py::test_secrets_backend_lifecycle ``` ``` tox -e integration -- tests/integration/test_crossmodel.py::test_relate_with_offer ``` ``` tox -e integration -- tests/integration/test_model.py::test_deploy_bundle_local_charms ``` ``` tox -e integration -- tests/integration/test_model.py::test_deploy_bundle_local_charm_series_manifest ``` ``` tox -e integration -- tests/integration/test_connection.py::test_reconnect ``` All CI tests need to pass. Following examples should complete without any noise: ```sh $ python examples/connect_current_model.py ``` Also try to deliberately have it error. The error should only include the relevant parts, i.e. no clutter or noise from pending tasks etc. ```sh $ juju deploy ubuntu $ python examples/deploy.py Connecting to model Deploying ubuntu Disconnecting from model Traceback (most recent call last): ........... File "/home/caner/work/python-libjuju/juju/client/_client17.py", line 1051, in Deploy reply = await self.rpc(msg) File "/home/caner/work/python-libjuju/juju/client/facade.py", line 659, in rpc result = await self.connection.rpc(msg, encoder=TypeEncoder) File "/home/caner/work/python-libjuju/juju/client/connection.py", line 693, in rpc raise errors.JujuError(err_results) juju.errors.JujuError: ['cannot add application "ubuntu": application already exists'] ``` #### Notes & Discussion JUJU-4549
2 parents eb21baa + 0b8912a commit 0f577e2

13 files changed

Lines changed: 101 additions & 55 deletions

File tree

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,4 @@ jobs:
102102
run: pip install tox
103103
- name: Run integration
104104
# Force one single concurrent test
105-
run: tox -e integration -- -n 1
105+
run: tox -e integration

juju/client/connection.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import logging
77
import ssl
8+
import signal
89
import urllib.request
910
import weakref
1011
from http.client import HTTPSConnection
@@ -217,7 +218,7 @@ def status(self):
217218
if connection.is_debug_log_connection:
218219
stopped = connection._debug_log_task.cancelled()
219220
else:
220-
stopped = connection._receiver_task.cancelled()
221+
stopped = connection._receiver_task is not None and connection._receiver_task.cancelled()
221222

222223
if stopped or not connection._ws.open:
223224
return self.ERROR
@@ -418,6 +419,14 @@ async def _open(self, endpoint, cacert):
418419
sock = self.proxy.socket()
419420
server_hostname = "juju-app"
420421

422+
def _exit_tasks():
423+
for task in jasyncio.all_tasks():
424+
task.cancel()
425+
426+
loop = jasyncio.get_running_loop()
427+
for sig in (signal.SIGINT, signal.SIGTERM):
428+
loop.add_signal_handler(sig, _exit_tasks)
429+
421430
return (await websockets.connect(
422431
url,
423432
ssl=self._get_ssl(cacert),
@@ -431,25 +440,41 @@ async def close(self, to_reconnect=False):
431440
return
432441
self.monitor.close_called.set()
433442

443+
# Cancel all the tasks (that we started):
434444
if self._pinger_task:
435445
self._pinger_task.cancel()
436-
self._pinger_task = None
437446
if self._receiver_task:
438447
self._receiver_task.cancel()
439-
self._receiver_task = None
440448
if self._debug_log_task:
441449
self._debug_log_task.cancel()
442-
self._debug_log_task = None
443-
# Allow a second for tasks to be cancelled
444-
await jasyncio.sleep(1)
445450

446451
if self._ws and not self._ws.closed:
447452
await self._ws.close()
448-
self._ws = None
453+
454+
if not to_reconnect:
455+
try:
456+
log.debug('Gathering all tasks for connection close')
457+
458+
# Avoid gathering the current task
459+
tasks_need_to_be_gathered = [task for task in jasyncio.all_tasks() if task != jasyncio.current_task()]
460+
await jasyncio.gather(*tasks_need_to_be_gathered)
461+
except jasyncio.CancelledError:
462+
pass
463+
except websockets.exceptions.ConnectionClosed:
464+
pass
465+
466+
self._pinger_task = None
467+
self._receiver_task = None
468+
self._debug_log_task = None
449469

450470
if self.proxy is not None:
451471
self.proxy.close()
452472

473+
# Remove signal handlers
474+
loop = jasyncio.get_running_loop()
475+
for sig in (signal.SIGINT, signal.SIGTERM):
476+
loop.remove_signal_handler(sig)
477+
453478
async def _recv(self, request_id):
454479
if not self.is_open:
455480
raise websockets.exceptions.ConnectionClosed(0, 'websocket closed')
@@ -517,15 +542,15 @@ async def _debug_logger(self):
517542
self.debug_log_shown_lines += number_of_lines_written
518543

519544
if self.debug_log_shown_lines >= self.debug_log_params['limit']:
520-
jasyncio.create_task(self.close())
545+
jasyncio.create_task(self.close(), name="Task_Close")
521546
return
522547

523548
except KeyError as e:
524549
log.exception('Unexpected debug line -- %s' % e)
525-
jasyncio.create_task(self.close())
550+
jasyncio.create_task(self.close(), name="Task_Close")
526551
raise
527552
except jasyncio.CancelledError:
528-
jasyncio.create_task(self.close())
553+
jasyncio.create_task(self.close(), name="Task_Close")
529554
raise
530555
except websockets.exceptions.ConnectionClosed:
531556
log.warning('Debug Logger: Connection closed, reconnecting')
@@ -536,7 +561,7 @@ async def _debug_logger(self):
536561
return
537562
except Exception as e:
538563
log.exception("Error in debug logger : %s" % e)
539-
jasyncio.create_task(self.close())
564+
jasyncio.create_task(self.close(), name="Task_Close")
540565
raise
541566

542567
async def _receiver(self):
@@ -552,7 +577,8 @@ async def _receiver(self):
552577
result = json.loads(result)
553578
await self.messages.put(result['request-id'], result)
554579
except jasyncio.CancelledError:
555-
raise
580+
log.debug('Receiver: Cancelled')
581+
pass
556582
except websockets.exceptions.ConnectionClosed as e:
557583
log.warning('Receiver: Connection closed, reconnecting')
558584
await self.messages.put_all(e)
@@ -592,7 +618,8 @@ async def _do_ping():
592618
break
593619
await jasyncio.sleep(10)
594620
except jasyncio.CancelledError:
595-
raise
621+
log.debug('Pinger: Cancelled')
622+
pass
596623
except websockets.exceptions.ConnectionClosed:
597624
# The connection has closed - we can't do anything
598625
# more until the connection is restarted.
@@ -769,7 +796,7 @@ async def reconnect(self):
769796
if not self.is_debug_log_connection:
770797
self._build_facades(res.get('facades', {}))
771798
if not self._pinger_task:
772-
self._pinger_task = jasyncio.create_task(self._pinger())
799+
self._pinger_task = jasyncio.create_task(self._pinger(), name="Task_Pinger")
773800

774801
async def _connect(self, endpoints):
775802
if len(endpoints) == 0:
@@ -820,12 +847,12 @@ async def _try_endpoint(endpoint, cacert, delay):
820847
# If this is a debug-log connection, and the _debug_log_task
821848
# is not created yet, then go ahead and schedule it
822849
if self.is_debug_log_connection and not self._debug_log_task:
823-
self._debug_log_task = jasyncio.create_task(self._debug_logger())
850+
self._debug_log_task = jasyncio.create_task(self._debug_logger(), name="Task_Debug_Log")
824851

825852
# If this is regular connection, and we dont have a
826853
# receiver_task yet, then schedule a _receiver_task
827854
elif not self.is_debug_log_connection and not self._receiver_task:
828-
self._receiver_task = jasyncio.create_task(self._receiver())
855+
self._receiver_task = jasyncio.create_task(self._receiver(), name="Task_Receiver")
829856

830857
log.debug("Driver connected to juju %s", self.addr)
831858
self.monitor.close_called.clear()
@@ -880,7 +907,7 @@ async def _connect_with_redirect(self, endpoints):
880907
login_result = await self._connect_with_login(e.endpoints)
881908
self._build_facades(login_result.get('facades', {}))
882909
if not self._pinger_task:
883-
self._pinger_task = jasyncio.create_task(self._pinger())
910+
self._pinger_task = jasyncio.create_task(self._pinger(), name="Task_Pinger")
884911

885912
# _build_facades takes the facade list that comes from the connection with the controller,
886913
# validates that the client knows about them (client_facades) and builds the facade list

juju/jasyncio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
gather, sleep, wait_for, create_subprocess_exec, subprocess, \
2222
wait, FIRST_COMPLETED, Lock, as_completed, new_event_loop, \
2323
get_event_loop_policy, CancelledError, get_running_loop, \
24-
create_task # noqa
24+
create_task, ALL_COMPLETED, all_tasks, current_task, shield # noqa
2525

2626

2727
def create_task_with_handler(coro, task_name, logger=ROOT_LOGGER):

juju/utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ async def block_until(*conditions, timeout=None, wait_period=0.5):
128128
async def _block():
129129
while not all(c() for c in conditions):
130130
await jasyncio.sleep(wait_period)
131-
await jasyncio.wait_for(_block(), timeout)
131+
await jasyncio.shield(jasyncio.wait_for(_block(), timeout))
132132

133133

134134
async def block_until_with_coroutine(condition_coroutine, timeout=None, wait_period=0.5):
@@ -139,7 +139,7 @@ async def block_until_with_coroutine(condition_coroutine, timeout=None, wait_per
139139
async def _block():
140140
while not await condition_coroutine():
141141
await jasyncio.sleep(wait_period)
142-
await jasyncio.wait_for(_block(), timeout=timeout)
142+
await jasyncio.shield(jasyncio.wait_for(_block(), timeout=timeout))
143143

144144

145145
async def wait_for_bundle(model, bundle, **kwargs):
@@ -181,6 +181,11 @@ async def run_with_interrupt(task, *events, log=None):
181181
return_when=jasyncio.FIRST_COMPLETED)
182182
for f in pending:
183183
f.cancel() # cancel unfinished tasks
184+
for f in pending:
185+
try:
186+
await f
187+
except jasyncio.CancelledError:
188+
pass
184189
for f in done:
185190
f.exception() # prevent "exception was not retrieved" errors
186191
if task in done:

tests/integration/a.file

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
options:
2+
status:
3+
type: string
4+
default: "active"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
3+
status="$(config-get status)"
4+
5+
if [[ "$status" == "error" ]]; then
6+
if [[ -e .errored ]]; then
7+
status="active"
8+
else
9+
touch .errored
10+
exit 1
11+
fi
12+
fi
13+
status-set "$status"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
bases:
2+
- architectures:
3+
- amd64
4+
channel: '22.04'
5+
name: ubuntu
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: assumes-charm
2+
summary: "test"
3+
description: "test"
4+
maintainers: ["test"]
5+
assumes:
6+
- juju
7+
- any-of:
8+
- all-of:
9+
- juju >= 2.9
10+
- juju < 3
11+
- all-of:
12+
- juju >= 3.1
13+
- juju < 4

tests/integration/test_connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def test_monitor(event_loop):
3131
assert conn.monitor.status == 'connected'
3232
await conn.close()
3333

34-
assert conn.monitor.status == 'disconnected'
34+
assert conn.monitor.status == 'disconnecting'
3535

3636

3737
@base.bootstrapped

0 commit comments

Comments
 (0)