Skip to content

Commit

Permalink
refactor: implement wait_and_log helper method to log periodically du…
Browse files Browse the repository at this point in the history
…ring waiting for all sub ECUs before reboot (#410)

This PR introduces `wait_and_log` helper method, which issues logs during waiting for flag set. otaclient now uses this method when waiting for all sub ECUs before reboot. 

Other changes:
1. otaclient will issue a warning log before actually calling reboot command.
2. otaclient will sleep 6 seconds before actually reboot the system.
  • Loading branch information
Bodong-Yang authored Nov 8, 2024
1 parent 95735c8 commit cfbd715
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 3 deletions.
11 changes: 10 additions & 1 deletion src/otaclient/app/ota_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
get_standby_slot_creator,
)
from otaclient.create_standby.common import DeltaBundle
from otaclient.utils import wait_and_log
from otaclient_api.v2 import types as api_types
from otaclient_common.common import ensure_otaproxy_start
from otaclient_common.downloader import (
Expand All @@ -68,6 +69,7 @@
logger = logging.getLogger(__name__)

DEFAULT_STATUS_QUERY_INTERVAL = 1
WAIT_BEFORE_REBOOT = 6


class LiveOTAStatus:
Expand Down Expand Up @@ -485,7 +487,14 @@ def _execute_update(self):
next(_postupdate_gen := self._boot_controller.post_update())

logger.info("local update finished, wait on all subecs...")
self._control_flags.wait_can_reboot_flag()
wait_and_log(
flag=self._control_flags._can_reboot,
message="permit reboot flag",
log_func=logger.info,
)

logger.info(f"device will reboot in {WAIT_BEFORE_REBOOT} seconds!")
time.sleep(WAIT_BEFORE_REBOOT)
next(_postupdate_gen, None) # reboot

# API
Expand Down
1 change: 1 addition & 0 deletions src/otaclient/boot_control/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ def reboot(cls, args: Optional[list[str]] = None) -> NoReturn:
cmd.extend(args)

try:
logger.warning("system will reboot now!")
subprocess_call(cmd, raise_exception=True)
sys.exit(0)
except CalledProcessError:
Expand Down
52 changes: 52 additions & 0 deletions src/otaclient/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2022 TIER IV, INC. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Common shared utils, only used by otaclient package."""


from __future__ import annotations

import itertools
import logging
import time
from abc import abstractmethod
from typing import Callable, Protocol

logger = logging.getLogger(__name__)


class CheckableFlag(Protocol):

@abstractmethod
def is_set(self) -> bool: ...


def wait_and_log(
flag: CheckableFlag,
message: str = "",
*,
check_interval: int = 2,
log_interval: int = 30,
log_func: Callable[[str], None] = logger.info,
) -> None:
"""Wait for <flag> until it is set while print a log every <log_interval>."""
log_round = 0
for seconds in itertools.count(step=check_interval):
if flag.is_set():
return

_new_log_round = seconds // log_interval
if _new_log_round > log_round:
log_func(f"wait for {message}: {seconds}s passed ...")
log_round = _new_log_round
time.sleep(check_interval)
4 changes: 3 additions & 1 deletion tests/test_otaclient/test_create_standby.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ def test_update_with_rebuild_mode(self, mocker: MockerFixture):
otaclient_control_flags = typing.cast(
OTAClientControlFlags, mocker.MagicMock(spec=OTAClientControlFlags)
)
otaclient_control_flags._can_reboot = _can_reboot = mocker.MagicMock()
_can_reboot.is_set = mocker.MagicMock(return_value=True)

ca_store = load_ca_cert_chains(cfg.CERTS_DIR)

Expand All @@ -116,7 +118,7 @@ def test_update_with_rebuild_mode(self, mocker: MockerFixture):
persist_handler.assert_called_once()
# --- assert update finished
_updater.shutdown.assert_called_once()
otaclient_control_flags.wait_can_reboot_flag.assert_called_once() # type: ignore
otaclient_control_flags._can_reboot.is_set.assert_called_once() # type: ignore
# --- ensure the update stats are collected
collector = _updater._update_stats_collector
assert collector.processed_files_num
Expand Down
5 changes: 4 additions & 1 deletion tests/test_otaclient/test_ota_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ def test_otaupdater(self, mocker: pytest_mock.MockerFixture):
otaclient_control_flags = typing.cast(
OTAClientControlFlags, mocker.MagicMock(spec=OTAClientControlFlags)
)
otaclient_control_flags._can_reboot = _can_reboot = mocker.MagicMock()
_can_reboot.is_set = mocker.MagicMock(return_value=True)

ca_store = load_ca_cert_chains(cfg.CERTS_DIR)

_updater = _OTAUpdater(
Expand All @@ -199,7 +202,7 @@ def test_otaupdater(self, mocker: pytest_mock.MockerFixture):
_downloaded_files_size += _f.stat().st_size
assert _downloaded_files_size == self._delta_bundle.total_download_files_size
# assert the control_flags has been waited
otaclient_control_flags.wait_can_reboot_flag.assert_called_once()
otaclient_control_flags._can_reboot.is_set.assert_called_once()
assert _updater.updating_version == str(cfg.UPDATE_VERSION)
# assert boot controller is used
self._boot_control.pre_update.assert_called_once()
Expand Down
55 changes: 55 additions & 0 deletions tests/test_otaclient/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2022 TIER IV, INC. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from __future__ import annotations

import logging
import time

import pytest

from otaclient.utils import wait_and_log

logger = logging.getLogger(__name__)


class _TickingFlag:

def __init__(self, trigger_in: int) -> None:
self._trigger_time = time.time() + trigger_in

def is_set(self) -> bool:
_now = time.time()
return _now > self._trigger_time


def test_wait_and_log(caplog: pytest.LogCaptureFixture):
# NOTE: allow 2 more seconds for expected_trigger_time
trigger_in, expected_trigger_time = 11, time.time() + 11 + 2
_flag = _TickingFlag(trigger_in=trigger_in)
_msg = "ticking flag"

wait_and_log(
_flag,
_msg,
check_interval=1,
log_interval=2,
log_func=logger.warning,
)

assert len(caplog.records) == 5
assert caplog.records[0].levelno == logging.WARNING
assert caplog.records[0].msg == f"wait for {_msg}: 2s passed ..."
assert time.time() < expected_trigger_time

1 comment on commit cfbd715

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/ota_metadata/legacy
   __init__.py110100% 
   parser.py3264386%100, 156, 161, 197–198, 208–209, 212, 224, 277–279, 283–286, 320–323, 392, 395, 403–405, 418, 427–428, 431–432, 597–599, 649–650, 653, 681–683, 737, 740–742
   types.py841384%37, 40–42, 112–116, 122–125
src/ota_metadata/utils
   cert_store.py420100% 
src/ota_proxy
   __init__.py361072%59, 61, 63, 72, 81–82, 102, 104–106
   __main__.py770%16–18, 20, 22–23, 25
   _consts.py150100% 
   cache_control_header.py68494%71, 91, 113, 121
   cache_streaming.py1421390%211, 225, 229–230, 265–266, 268, 280, 348, 366–369
   config.py170100% 
   db.py731875%109, 115, 153, 159–160, 163, 169, 171, 192–199, 201–202
   errors.py50100% 
   lru_cache_helper.py47295%84–85
   ota_cache.py2155972%70–71, 140, 151–152, 184–185, 202, 239–243, 247–249, 251, 253–260, 262–264, 267–268, 272–273, 277, 324, 332–334, 413–416, 430, 433–434, 448–449, 451–453, 457–458, 464–465, 496, 502, 529, 581–583
   server_app.py1393971%76, 79, 85, 101, 103, 162, 171, 213–214, 216–218, 221, 226–228, 231–232, 235, 238, 241, 244, 257–258, 261–262, 264, 267, 293–296, 299, 313–315, 321–323
   utils.py140100% 
src/otaclient
   __init__.py5260%17, 19
   __main__.py110%16
   _types.py74740%17, 19–20, 22, 24–25, 32–34, 37–39, 42–49, 52–58, 61–64, 72–79, 82–90, 93–99, 107–108, 111–113, 115–122, 125–126, 129–131, 134
   log_setting.py52590%53, 55, 64–66
   utils.py170100% 
src/otaclient/app
   __main__.py110%16
   configs.py750100% 
   errors.py1200100% 
   interface.py30100% 
   main.py46589%52–53, 75–77
   ota_client.py38711370%86, 94, 115, 142, 144–145, 147, 151, 155–156, 161–162, 168, 170, 211–214, 220, 224, 230, 349, 361–362, 364, 373, 376, 381–382, 385, 391, 393–397, 416–419, 422–429, 457–460, 513–514, 518, 520–521, 551–552, 561–568, 575, 578–584, 632–634, 636, 638–641, 649, 664, 693–695, 700–702, 705–706, 708–709, 711, 769–770, 773, 781–782, 785, 796–797, 800, 808–809, 812, 823, 842, 869, 888, 906
   ota_client_stub.py39410972%76–78, 80–81, 89–92, 95–97, 101, 106–107, 109–110, 113, 115–116, 119–121, 124–125, 128–130, 135–140, 144, 147–151, 153–154, 162–164, 167, 204–206, 211, 247, 272, 275, 278, 382, 406, 408, 432, 478, 535, 605–606, 645, 664–666, 672–675, 679–681, 688–690, 693, 697–700, 753, 842–844, 851, 881–882, 885–889, 898–907, 914, 920, 923–924, 928, 931
   update_stats.py104991%57, 103, 105, 114, 116, 125, 127, 148, 179
src/otaclient/boot_control
   __init__.py40100% 
   _common.py25011354%70–71, 92–94, 110–111, 131–132, 151–152, 171–172, 191–192, 214–216, 231–232, 256–262, 283, 291, 309, 317, 336–337, 340–341, 364, 366–375, 377–386, 388–390, 409, 412, 420, 428, 444–446, 448–454, 547, 552, 557, 670, 674–675, 678, 685, 687–688, 714–715, 717–720, 725, 731–732, 735–736, 738, 745–746, 757–763, 773–775, 779–780, 783–784, 787, 793
   _firmware_package.py932276%82, 86, 136, 180, 186, 209–210, 213–218, 220–221, 224–229, 231
   _grub.py41812869%216, 264–267, 273–277, 314–315, 322–327, 330–336, 339, 342–343, 348, 350–352, 361–367, 369–370, 372–374, 383–385, 387–389, 468–469, 473–474, 526, 532, 558, 580, 584–585, 600–602, 626–629, 641, 645–647, 649–651, 710–713, 738–741, 764–767, 779–780, 783–784, 819, 825, 845–846, 848, 860, 863, 866, 869, 873–875, 893–896, 924–927, 932–940, 945–953
   _jetson_cboot.py2632630%20, 22–25, 27–29, 35–39, 41–42, 58–59, 61, 63–64, 70, 74, 133, 136, 138–139, 142, 149–150, 158–159, 162, 168–169, 177, 186–190, 192, 198, 201–202, 208, 211–212, 217–218, 220, 226–227, 230–231, 234–236, 238, 244, 249–251, 253–255, 260, 262–265, 267–268, 277–278, 281–282, 287–288, 291–295, 298–299, 304–305, 308, 311–315, 320–323, 326, 329–330, 333, 336–337, 340, 344–349, 353–354, 358, 361–362, 365, 368–371, 373, 376–377, 381, 384, 387–390, 392, 399, 403–404, 407–408, 414–415, 421, 423–424, 428, 430, 432–434, 437, 441, 444, 447–448, 450, 453, 461–462, 469, 479, 482, 490–491, 496–499, 501, 508, 510–512, 518–519, 523–524, 527, 531, 534, 536, 543–547, 549, 561–564, 567, 570, 572, 579, 583–584, 586–587, 589–591, 593, 595, 598, 601, 604, 606–607, 610–614, 618–620, 622, 630–634, 636, 639, 643, 646, 657–658, 663, 673, 676–684, 688–697, 701–710, 714, 716–718, 720–721, 723–724
   _jetson_common.py1734573%133, 141, 289–292, 295, 312, 320, 355, 360–365, 383, 409–410, 412–414, 418–421, 423–424, 426–430, 432, 439–440, 443–444, 454, 457–458, 461, 463, 507–508
   _jetson_uefi.py39827131%123–125, 130–131, 150–152, 157–160, 327, 445, 447–450, 454, 458–459, 461–469, 471, 483–484, 487–488, 491–492, 495–497, 501–502, 507–509, 513, 517–518, 521–522, 525–526, 530, 533–534, 536, 541–542, 546, 549–550, 555, 559–560, 563, 567–569, 571, 575–578, 580–581, 603–604, 608–609, 611, 615, 619–620, 623–624, 631, 634–636, 639, 641–642, 647–648, 651–654, 656–657, 662, 664–665, 673, 676–679, 681–682, 684, 688–689, 693, 701–705, 708–709, 711, 714–718, 721, 724–728, 732–733, 736–741, 744–745, 748–751, 753–754, 761–762, 772–775, 778, 781–784, 787–791, 794–795, 798, 801–804, 807, 809, 814–815, 818, 821–824, 826, 832, 837–838, 857–858, 861, 869–870, 877, 887, 890, 897–898, 903–906, 914–917, 925–926, 938–941, 943, 946, 949, 957, 968–970, 972–974, 976–980, 985–986, 988, 1001, 1005, 1008, 1018, 1023, 1031–1032, 1035, 1039, 1041–1043, 1049–1050, 1055, 1063–1070, 1075–1083, 1088–1096, 1102–1104
   _rpi_boot.py28713453%54, 57, 121–122, 126, 134–137, 151–154, 161–162, 164–165, 170–171, 174–175, 184–185, 223, 229–233, 236, 254–256, 260–262, 267–269, 273–275, 285–286, 289, 292, 294–295, 297–298, 300–302, 308, 311–312, 322–325, 333–337, 339, 341–342, 347–348, 355–361, 392, 394–397, 407–410, 414–415, 417–421, 449–452, 471–474, 479, 482, 500–503, 508–516, 521–529, 546–549, 555–557, 560, 563
   configs.py550100% 
   protocol.py40100% 
   selecter.py412929%45–47, 50–51, 55–56, 59–61, 64, 66, 70, 78–80, 82–83, 85–86, 90, 92, 94–95, 97, 99–100, 102, 104
src/otaclient/configs
   __init__.py50100% 
   _cfg_configurable.py510100% 
   _cfg_consts.py44197%92
   _common.py80100% 
   _ecu_info.py56492%59, 64–65, 112
   _proxy_info.py51296%88, 90
   cfg.py19384%54–56
src/otaclient/create_standby
   __init__.py12558%29–31, 33, 35
   common.py2244480%62, 65–66, 70–72, 74, 78–79, 81, 127, 175–177, 179–181, 183, 186–189, 193, 204, 278–279, 281–286, 298, 335, 363, 366–368, 384–385, 399, 403, 425–426
   interface.py50100% 
   rebuild_mode.py97990%93–95, 107–112
src/otaclient_api/v2
   api_caller.py39684%45–47, 83–85
   api_stub.py170100% 
   types.py2562391%86, 89–92, 131, 209–210, 212, 259, 262–263, 506–508, 512–513, 515, 518–519, 522–523, 586
src/otaclient_common
   __init__.py34876%42–44, 61, 63, 69, 76–77
   _io.py64198%41
   common.py1061189%44, 148, 151–153, 168, 175–177, 271, 275
   downloader.py1991094%107–108, 126, 153, 369, 424, 428, 516–517, 526
   linux.py611575%51–53, 59, 69, 74, 76, 108–109, 133–134, 190, 195–196, 198
   logging.py29196%55
   persist_file_handling.py1181884%113, 118, 150–152, 163, 192–193, 228–232, 242–244, 246–247
   proto_streamer.py42880%33, 48, 66–67, 72, 81–82, 100
   proto_wrapper.py3984887%87, 165, 172, 184–186, 205, 210, 221, 257, 263, 268, 299, 303, 307, 402, 462, 469, 472, 492, 499, 501, 526, 532, 535, 537, 562, 568, 571, 573, 605, 609, 611, 625, 642, 669, 672, 676, 707, 713, 760–763, 765, 803–805
   retry_task_map.py105595%158–159, 161, 181–182
   typing.py31487%48, 97–98, 100
TOTAL6577175873% 

Tests Skipped Failures Errors Time
232 0 💤 0 ❌ 0 🔥 13m 55s ⏱️

Please sign in to comment.