Implement Docker healthcheck functionality and improve progress message handling
All checks were successful
Publish Twitch Archive Container / publish (push) Successful in 1m30s
All checks were successful
Publish Twitch Archive Container / publish (push) Successful in 1m30s
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
cd3e37ff59
commit
9d5c707646
5 changed files with 96 additions and 7 deletions
|
|
@ -10,7 +10,7 @@ services:
|
|||
command:
|
||||
- sh
|
||||
- -lc
|
||||
- python twitch-archive.py ${TWITCH_ARCHIVE_ARGS:--u vinesauce --verbose}
|
||||
- exec python -u twitch-archive.py ${TWITCH_ARCHIVE_ARGS:--u vinesauce --verbose}
|
||||
volumes:
|
||||
- .:/app
|
||||
- ${TWITCH_ARCHIVE_ARCHIVE_BIND:-./archive}:/app/archive
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ services:
|
|||
command:
|
||||
- sh
|
||||
- -lc
|
||||
- python twitch-archive.py ${TWITCH_ARCHIVE_ARGS:--u vinesauce}
|
||||
- exec python -u twitch-archive.py ${TWITCH_ARCHIVE_ARGS:--u vinesauce}
|
||||
volumes:
|
||||
- ${TWITCH_ARCHIVE_ARCHIVE_BIND:-./archive}:/app/archive
|
||||
- ${TWITCH_ARCHIVE_CONFIG_BIND:-./config}:/app/config
|
||||
|
|
|
|||
|
|
@ -612,7 +612,10 @@ class ContentDownloader:
|
|||
|
||||
# Show progress every 100 messages
|
||||
if message_count % 100 == 0:
|
||||
print(f'{Fore.CYAN} Downloaded {message_count} messages...{Style.RESET_ALL}', end='\r')
|
||||
if sys.stdout.isatty():
|
||||
print(f'{Fore.CYAN} Downloaded {message_count} messages...{Style.RESET_ALL}', end='\r', flush=True)
|
||||
else:
|
||||
print(f'{Fore.CYAN} Downloaded {message_count} messages...{Style.RESET_ALL}', flush=True)
|
||||
|
||||
# Show chat previews in verbose mode (every 10 messages)
|
||||
if verbose and message_count % 10 == 0:
|
||||
|
|
|
|||
|
|
@ -767,6 +767,55 @@ class TestEnvironmentLoadingRegression(unittest.TestCase):
|
|||
self.module.TwitchArchive._load_environment_variables(archive)
|
||||
|
||||
|
||||
class TestHealthcheckRegression(unittest.TestCase):
|
||||
"""Regression tests for Docker healthcheck behavior."""
|
||||
|
||||
def setUp(self):
|
||||
self.module = load_twitch_archive_module()
|
||||
|
||||
@patch.object(os, 'getenv', side_effect=lambda name, default=None: default)
|
||||
def test_run_healthcheck_fails_when_heartbeat_is_missing(self, _mock_getenv):
|
||||
config_manager = MagicMock()
|
||||
config_manager.get_all_enabled_streamers.return_value = ['maddoscientist0']
|
||||
config_manager.load_streamer_config.return_value = {'downloadVOD': False, 'downloadCHAT': False, 'uploadCloud': False}
|
||||
|
||||
archive = MagicMock()
|
||||
archive.os_type = 'linux'
|
||||
archive.downloadVOD = False
|
||||
archive.downloadCHAT = False
|
||||
archive.uploadCloud = False
|
||||
|
||||
with patch.object(self.module, 'ConfigManager', return_value=config_manager), \
|
||||
patch.object(self.module, 'TwitchArchive', return_value=archive), \
|
||||
patch.object(self.module, 'verify_streamlink', return_value=True), \
|
||||
patch.object(self.module, 'verify_ffmpeg', return_value=True), \
|
||||
patch.object(self.module, 'has_fresh_healthcheck_heartbeat', return_value=False):
|
||||
result = self.module.run_healthcheck('maddoscientist0')
|
||||
|
||||
self.assertEqual(result, 1)
|
||||
|
||||
@patch.object(os, 'getenv', side_effect=lambda name, default=None: default)
|
||||
def test_run_healthcheck_succeeds_with_fresh_heartbeat(self, _mock_getenv):
|
||||
config_manager = MagicMock()
|
||||
config_manager.get_all_enabled_streamers.return_value = ['maddoscientist0']
|
||||
config_manager.load_streamer_config.return_value = {'downloadVOD': False, 'downloadCHAT': False, 'uploadCloud': False}
|
||||
|
||||
archive = MagicMock()
|
||||
archive.os_type = 'linux'
|
||||
archive.downloadVOD = False
|
||||
archive.downloadCHAT = False
|
||||
archive.uploadCloud = False
|
||||
|
||||
with patch.object(self.module, 'ConfigManager', return_value=config_manager), \
|
||||
patch.object(self.module, 'TwitchArchive', return_value=archive), \
|
||||
patch.object(self.module, 'verify_streamlink', return_value=True), \
|
||||
patch.object(self.module, 'verify_ffmpeg', return_value=True), \
|
||||
patch.object(self.module, 'has_fresh_healthcheck_heartbeat', return_value=True):
|
||||
result = self.module.run_healthcheck('maddoscientist0')
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run tests with verbose output
|
||||
print("="*70)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,33 @@ from modules.downloader import ContentDownloader
|
|||
from modules.file_manager import FileManager
|
||||
|
||||
|
||||
HEALTHCHECK_HEARTBEAT_PATH = os.getenv('TWITCH_ARCHIVE_HEARTBEAT_PATH', '/tmp/twitch-archive-heartbeat')
|
||||
HEALTHCHECK_MAX_AGE_SECONDS = int(os.getenv('TWITCH_ARCHIVE_HEALTHCHECK_MAX_AGE', '180'))
|
||||
|
||||
|
||||
def write_healthcheck_heartbeat() -> None:
|
||||
"""Record a recent application heartbeat for Docker health checks."""
|
||||
pathlib.Path(HEALTHCHECK_HEARTBEAT_PATH).touch()
|
||||
|
||||
|
||||
def has_fresh_healthcheck_heartbeat(max_age_seconds: int = HEALTHCHECK_MAX_AGE_SECONDS) -> bool:
|
||||
"""Return whether the application heartbeat file exists and is recent."""
|
||||
try:
|
||||
heartbeat_age = time.time() - os.path.getmtime(HEALTHCHECK_HEARTBEAT_PATH)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
return heartbeat_age <= max_age_seconds
|
||||
|
||||
|
||||
def print_progress_line(message: str) -> None:
|
||||
"""Use carriage returns only in an interactive terminal so Docker logs keep full lines."""
|
||||
if sys.stdout.isatty():
|
||||
print(message, end='\r', flush=True)
|
||||
else:
|
||||
print(message, flush=True)
|
||||
|
||||
|
||||
class TwitchArchive:
|
||||
"""
|
||||
Main class for the Twitch Archive system.
|
||||
|
|
@ -345,16 +372,19 @@ class TwitchArchive:
|
|||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
if hasattr(signal, 'SIGTERM'):
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
||||
write_healthcheck_heartbeat()
|
||||
|
||||
while not self.shutdown_requested:
|
||||
try:
|
||||
write_healthcheck_heartbeat()
|
||||
# Check stream status using StreamMonitor
|
||||
response = self.stream_monitor.check_stream_status()
|
||||
is_live = response['data']['user']['stream']
|
||||
|
||||
# Stream is offline
|
||||
if is_live is None:
|
||||
print(f'{Fore.CYAN}⏳ {self.username} is offline. Checking again in {self.refresh}s...{Style.RESET_ALL}', end='\r')
|
||||
print_progress_line(f'{Fore.CYAN}⏳ {self.username} is offline. Checking again in {self.refresh}s...{Style.RESET_ALL}')
|
||||
if self.shutdown_requested:
|
||||
break
|
||||
self._interruptible_sleep(self.refresh)
|
||||
|
|
@ -522,7 +552,7 @@ class TwitchArchive:
|
|||
|
||||
# Wait before checking again
|
||||
if not vod_found:
|
||||
print(f'{Fore.CYAN}VOD not found yet, waiting...{Style.RESET_ALL}', end='\r')
|
||||
print_progress_line(f'{Fore.CYAN}VOD not found yet, waiting...{Style.RESET_ALL}')
|
||||
if not self._interruptible_sleep(min(10, self.vodTimeout - (time.time() - vod_wait_start))):
|
||||
break
|
||||
|
||||
|
|
@ -941,6 +971,8 @@ class TwitchArchiveManager:
|
|||
# Print configuration summary for each streamer
|
||||
for username, archiver in self.archivers.items():
|
||||
archiver._print_configuration_summary()
|
||||
|
||||
write_healthcheck_heartbeat()
|
||||
|
||||
print(f'\n{Fore.GREEN}🚀 Starting monitoring loop...{Style.RESET_ALL}\n')
|
||||
|
||||
|
|
@ -958,6 +990,7 @@ class TwitchArchiveManager:
|
|||
|
||||
while not self.shutdown_requested:
|
||||
current_time = time.time()
|
||||
write_healthcheck_heartbeat()
|
||||
|
||||
# Print periodic status every 60 seconds
|
||||
if current_time - last_status_print >= 60:
|
||||
|
|
@ -1037,7 +1070,7 @@ class TwitchArchiveManager:
|
|||
else:
|
||||
# Not live
|
||||
if self.verbose:
|
||||
print(f'{Fore.CYAN}[{username}] Offline - checking again in {archiver.refresh}s{Style.RESET_ALL}', end='\r')
|
||||
print_progress_line(f'{Fore.CYAN}[{username}] Offline - checking again in {archiver.refresh}s{Style.RESET_ALL}')
|
||||
|
||||
except Exception as e:
|
||||
print(f'{Fore.RED}[{username}] Error checking stream: {e}{Style.RESET_ALL}')
|
||||
|
|
@ -1340,7 +1373,7 @@ class TwitchArchiveManager:
|
|||
|
||||
# Wait before checking again
|
||||
if not vod_found:
|
||||
print(f'{Fore.CYAN}VOD not found yet, waiting...{Style.RESET_ALL}', end='\r')
|
||||
print_progress_line(f'{Fore.CYAN}VOD not found yet, waiting...{Style.RESET_ALL}')
|
||||
time.sleep(min(10, archiver.vodTimeout - (time.time() - vod_wait_start)))
|
||||
|
||||
if not vod_found:
|
||||
|
|
@ -1518,6 +1551,10 @@ def run_healthcheck(specific_streamer: Optional[str] = None) -> int:
|
|||
if not checks_ok:
|
||||
return 1
|
||||
|
||||
if not has_fresh_healthcheck_heartbeat():
|
||||
print(f'{Fore.RED}✗ ERROR: Application heartbeat is missing or stale at {HEALTHCHECK_HEARTBEAT_PATH}{Style.RESET_ALL}')
|
||||
return 1
|
||||
|
||||
print(f'{Fore.GREEN}✓ Healthcheck OK for {username}{Style.RESET_ALL}')
|
||||
return 0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue