Skip to content

Pgsql fix: Add TCP keepalives so dead connections are detected in ~80s#231

Merged
ralftar merged 1 commit intomainfrom
ralftar/pgsql-tcp-keepalives
Apr 22, 2026
Merged

Pgsql fix: Add TCP keepalives so dead connections are detected in ~80s#231
ralftar merged 1 commit intomainfrom
ralftar/pgsql-tcp-keepalives

Conversation

@ralftar
Copy link
Copy Markdown
Owner

@ralftar ralftar commented Apr 22, 2026

Problem

Azure PostgreSQL (and its proxy layer) can drop idle TCP connections without sending RST. With Linux's default tcp_keepalive_time = 7200, pg_dump sits on a read syscall for ~2 hours before the kernel notices the socket is dead. Meanwhile:

  • The 3-attempt retry loop is effectively useless — each attempt can burn ~2h before failing.
  • Any downstream jobs that run serially after pgsql never get a chance to start in a reasonable window.
  • The dump file grows to some partial size, then sits frozen while the process waits on a dead socket.

Change

Pass libpq TCP keepalive parameters via a conninfo string to both psql (database listing) and pg_dump:

  • keepalives=1 — enable TCP keepalive on the socket.
  • keepalives_idle=30 — start probing after 30s idle.
  • keepalives_interval=10 — probe every 10s.
  • keepalives_count=5 — give up after 5 failed probes.

Net effect: a dead socket is detected in ~80s instead of ~2h. The existing retry loop (db_retries, default 3) can then actually retry within a useful time budget.

Short-lived connectivity checks in config.py (verify_pgsql, etc.) use PGCONNECT_TIMEOUT=5 and don't need keepalives — left unchanged to keep the diff minimal.

Test plan

  • pytest tests/test_runners.py tests/test_unit.py — 210 passed, 7 skipped
  • ruff check clouddump tests — clean
  • CI integration tests

Azure PostgreSQL silently drops idle TCP connections without sending RST.
With Linux's default 2h tcp_keepalive_time, pg_dump hangs on a read for
~2 hours before failing, which exhausts retries and delays downstream jobs.

Pass keepalive params via libpq conninfo (keepalives_idle=30,
keepalives_interval=10, keepalives_count=5) so dead sockets are detected
in ~80s and the retry loop can actually make progress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 22, 2026

CI Results

Test Status Details
Unit tests ✅ success 217 passed in 2.82s, coverage 61%
Docker build + smoke ✅ success 8 passed, 0 failed (build 42s)
Integration tests ✅ success 17 passed, 0 failed in 1m 1s
Tool smoke tests

Tool Smoke Tests

  • github-backup
  • git
  • aws
  • azcopy
  • pg_dump
  • psql
  • mysqldump
  • mysql

8 passed, 0 failed

Unit test output
============================= test session starts ==============================
platform linux -- Python 3.12.13, pytest-9.0.3, pluggy-1.6.0 -- /opt/hostedtoolcache/Python/3.12.13/x64/bin/python
cachedir: .pytest_cache
rootdir: /home/runner/work/CloudDump/CloudDump
plugins: cov-7.1.0
collecting ... collected 217 items

tests/test_unit.py::test_log_format_info PASSED                          [  0%]
tests/test_unit.py::test_log_format_warning PASSED                       [  0%]
tests/test_unit.py::test_log_format_error PASSED                         [  1%]
tests/test_unit.py::test_log_format_critical PASSED                      [  1%]
tests/test_unit.py::test_log_format_restores_levelname PASSED            [  2%]
tests/test_unit.py::test_log_format_redacts_secrets[password=SuperSecret123-SuperSecret123] PASSED [  2%]
tests/test_unit.py::test_log_format_redacts_secrets[token=ghp_abc123secret-ghp_abc123secret] PASSED [  3%]
tests/test_unit.py::test_log_format_redacts_secrets[AKIAIOSFODNN7EXAMPLE-AKIAIOSFODNN7EXAMPLE] PASSED [  3%]
tests/test_unit.py::test_log_format_redacts_secrets[AccountKey=abc123;EndpointSuffix=core.windows.net-abc123] PASSED [  4%]
tests/test_unit.py::test_log_format_redacts_secrets[postgres://admin:s3cret@db:5432/mydb-s3cret] PASSED [  4%]
tests/test_unit.py::test_log_format_includes_job_context PASSED          [  5%]
tests/test_unit.py::test_log_format_no_prefix_without_job PASSED         [  5%]
tests/test_unit.py::test_log_format_restores_msg_after_job_context PASSED [  5%]
tests/test_unit.py::test_log_format_extra_fields PASSED                  [  6%]
tests/test_unit.py::test_log_format_unknown_extra_fields_excluded PASSED [  6%]
tests/test_unit.py::test_text_formatter_output PASSED                    [  7%]
tests/test_unit.py::test_debug_lines_suppressed_at_info_level PASSED     [  7%]
tests/test_unit.py::test_debug_lines_visible_at_debug_level PASSED       [  8%]
tests/test_unit.py::test_tool_output_logged_at_debug_level PASSED        [  8%]
tests/test_unit.py::test_validate_cron_valid[* * * * *] PASSED           [  9%]
tests/test_unit.py::test_validate_cron_valid[*/5 * * * *] PASSED         [  9%]
tests/test_unit.py::test_validate_cron_valid[0 3 * * *] PASSED           [ 10%]
tests/test_unit.py::test_validate_cron_valid[59 23 31 12 6] PASSED       [ 10%]
tests/test_unit.py::test_validate_cron_valid[0 1-5 * * *] PASSED         [ 11%]
tests/test_unit.py::test_validate_cron_valid[0,30 * * * *] PASSED        [ 11%]
tests/test_unit.py::test_validate_cron_valid[0 3 1,15 * *] PASSED        [ 11%]
tests/test_unit.py::test_validate_cron_valid[0 3 * * 1-5] PASSED         [ 12%]
tests/test_unit.py::test_validate_cron_valid[*/10 9-17 * * *] PASSED     [ 12%]
tests/test_unit.py::test_validate_cron_valid[* * * * 7] PASSED           [ 13%]
tests/test_unit.py::test_validate_cron_invalid[* * *] PASSED             [ 13%]
tests/test_unit.py::test_validate_cron_invalid[* * * * * *] PASSED       [ 14%]
tests/test_unit.py::test_validate_cron_invalid[60 * * * *] PASSED        [ 14%]
tests/test_unit.py::test_validate_cron_invalid[* 24 * * *] PASSED        [ 15%]
tests/test_unit.py::test_validate_cron_invalid[* * 0 * *] PASSED         [ 15%]
tests/test_unit.py::test_validate_cron_invalid[* * * 13 *] PASSED        [ 16%]
tests/test_unit.py::test_validate_cron_invalid[*/0 * * * *] PASSED       [ 16%]
tests/test_unit.py::test_validate_cron_invalid[abc * * * *] PASSED       [ 17%]
tests/test_unit.py::test_matches_cron[30 14 * * *-dt0-True] PASSED       [ 17%]
tests/test_unit.py::test_matches_cron[30 14 * * *-dt1-False] PASSED      [ 17%]
tests/test_unit.py::test_matches_cron[*/15 * * * *-dt2-True] PASSED      [ 18%]
tests/test_unit.py::test_matches_cron[*/15 * * * *-dt3-False] PASSED     [ 18%]
tests/test_unit.py::test_matches_cron[* * * * 0-dt4-True] PASSED         [ 19%]
tests/test_unit.py::test_matches_cron[0 9-17 * * *-dt5-True] PASSED      [ 19%]
tests/test_unit.py::test_matches_cron[0 9-17 * * *-dt6-False] PASSED     [ 20%]
tests/test_unit.py::test_matches_cron[0,30 * * * *-dt7-True] PASSED      [ 20%]
tests/test_unit.py::test_matches_cron[0,30 * * * *-dt8-False] PASSED     [ 21%]
tests/test_unit.py::test_matches_cron[0 3 * * 1-5-dt9-True] PASSED       [ 21%]
tests/test_unit.py::test_matches_cron[0 3 * * 1-5-dt10-False] PASSED     [ 22%]
tests/test_unit.py::test_should_run_matches PASSED                       [ 22%]
tests/test_unit.py::test_should_run_no_match PASSED                      [ 23%]
tests/test_unit.py::test_should_run_no_double_fire PASSED                [ 23%]
tests/test_unit.py::test_should_run_next_match PASSED                    [ 23%]
tests/test_unit.py::test_should_run_missed_slot_waits PASSED             [ 24%]
tests/test_unit.py::test_validate_settings_valid_crontab PASSED          [ 24%]
tests/test_unit.py::test_validate_settings_missing_crontab PASSED        [ 25%]
tests/test_unit.py::test_validate_settings_invalid_crontab PASSED        [ 25%]
tests/test_unit.py::test_validate_settings_bad_bool PASSED               [ 26%]
tests/test_unit.py::test_validate_settings_valid_health_port PASSED      [ 26%]
tests/test_unit.py::test_validate_settings_bad_health_port[abc] PASSED   [ 27%]
tests/test_unit.py::test_validate_settings_bad_health_port[0] PASSED     [ 27%]
tests/test_unit.py::test_validate_settings_bad_health_port[-1] PASSED    [ 28%]
tests/test_unit.py::test_validate_settings_bad_health_port[70000] PASSED [ 28%]
tests/test_unit.py::test_validate_jobs_valid PASSED                      [ 29%]
tests/test_unit.py::test_validate_jobs_rejects[overrides0] PASSED        [ 29%]
tests/test_unit.py::test_validate_jobs_rejects[overrides1] PASSED        [ 29%]
tests/test_unit.py::test_validate_jobs_rejects[overrides2] PASSED        [ 30%]
tests/test_unit.py::test_validate_jobs_rejects[overrides3] PASSED        [ 30%]
tests/test_unit.py::test_validate_jobs_rejects[overrides4] PASSED        [ 31%]
tests/test_unit.py::test_validate_jobs_rejects[overrides5] PASSED        [ 31%]
tests/test_unit.py::test_validate_jobs_github_valid PASSED               [ 32%]
tests/test_unit.py::test_validate_jobs_mysql_valid PASSED                [ 32%]
tests/test_unit.py::test_validate_jobs_duplicate_id PASSED               [ 33%]
tests/test_unit.py::test_validate_jobs_disabled_tag_in_summary PASSED    [ 33%]
tests/test_unit.py::test_verify_connectivity_skips_disabled_job PASSED   [ 34%]
tests/test_unit.py::test_redact_strips_secrets[password: secret123-secret123] PASSED [ 34%]
tests/test_unit.py::test_redact_strips_secrets[pass: abc-abc] PASSED     [ 35%]
tests/test_unit.py::test_redact_strips_secrets[token=xyz-xyz] PASSED     [ 35%]
tests/test_unit.py::test_redact_strips_secrets["pass": "SuperSecret123"-SuperSecret123] PASSED [ 35%]
tests/test_unit.py::test_redact_strips_secrets["password": "db_pass_456"-db_pass_456] PASSED [ 36%]
tests/test_unit.py::test_redact_strips_secrets["token": "ghp_xxx"-ghp_xxx] PASSED [ 36%]
tests/test_unit.py::test_redact_strips_secrets["secret": "my-api-secret"-my-api-secret] PASSED [ 37%]
tests/test_unit.py::test_redact_strips_secrets["aws_secret_access_key": "wJalrXUtnFEMI"-wJalrXUtnFEMI] PASSED [ 37%]
tests/test_unit.py::test_redact_strips_secrets[key is AKIAIOSFODNN7EXAMPLE here-AKIAIOSFODNN7EXAMPLE] PASSED [ 38%]
tests/test_unit.py::test_redact_strips_secrets[Error: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn-ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn] PASSED [ 38%]
tests/test_unit.py::test_redact_strips_secrets[Error: ghu_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn-ghu_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn] PASSED [ 39%]
tests/test_unit.py::test_redact_strips_secrets[Error: ghs_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn-ghs_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn] PASSED [ 39%]
tests/test_unit.py::test_redact_strips_secrets[AccountKey=abc123;EndpointSuffix=core.windows.net-abc123] PASSED [ 40%]
tests/test_unit.py::test_redact_strips_secrets[https://store.blob.core.windows.net/c?sv=2021-08&sig=abc&se=2025-01-01-sig=abc] PASSED [ 40%]
tests/test_unit.py::test_redact_strips_secrets[https://store.blob.core.windows.net/c?SV=2021&SIG=abc&SE=2025-SIG=abc] PASSED [ 41%]
tests/test_unit.py::test_redact_strips_secrets[https://store.blob.core.windows.net/c?sv=2021&Sig=abc&se=2025-Sig=abc] PASSED [ 41%]
tests/test_unit.py::test_redact_strips_secrets[postgres://admin:s3cret@db.example.com:5432/mydb-s3cret] PASSED [ 41%]
tests/test_unit.py::test_redact_strips_secrets[postgresql://user:hunter2@localhost/app-hunter2] PASSED [ 42%]
tests/test_unit.py::test_redact_strips_secrets[mongodb://root:mongopass@mongo:27017-mongopass] PASSED [ 42%]
tests/test_unit.py::test_redact_strips_secrets[mysql://root:p%40ssw0rd@db:3306/mydb-p%40ssw0rd] PASSED [ 43%]
tests/test_unit.py::test_redact_strips_secrets[Authorization: Bearer ghp_abc123secret-ghp_abc123secret] PASSED [ 43%]
tests/test_unit.py::test_redact_strips_secrets[authorization: token mytoken123-mytoken123] PASSED [ 44%]
tests/test_unit.py::test_redact_strips_pem_private_key PASSED            [ 44%]
tests/test_unit.py::test_redact_ignores_clean_text PASSED                [ 45%]
tests/test_unit.py::test_redact_sas_preserves_json_structure_across_multiple_urls PASSED [ 45%]
tests/test_unit.py::test_validate_jobs_github_invalid_account_type PASSED [ 46%]
tests/test_unit.py::test_verify_connectivity_github_token PASSED         [ 46%]
tests/test_unit.py::test_verify_connectivity_github_warns_on_failure PASSED [ 47%]
tests/test_unit.py::test_verify_connectivity_db_connection PASSED        [ 47%]
tests/test_unit.py::test_verify_connectivity_db_connection_failure PASSED [ 47%]
tests/test_unit.py::test_verify_s3_bucket_ok PASSED                      [ 48%]
tests/test_unit.py::test_verify_s3_bucket_failure PASSED                 [ 48%]
tests/test_unit.py::test_verify_az_container_ok PASSED                   [ 49%]
tests/test_unit.py::test_verify_az_container_failure PASSED              [ 49%]
tests/test_unit.py::test_verify_rsync_ssh_ok PASSED                      [ 50%]
tests/test_unit.py::test_verify_rsync_ssh_failure PASSED                 [ 50%]
tests/test_unit.py::test_verify_rsync_ssh_ok_ignores_host_key_notice PASSED [ 51%]
tests/test_unit.py::test_verify_rsync_ssh_failure_skips_host_key_notice PASSED [ 51%]
tests/test_unit.py::test_verify_pgsql_databases_and_tables PASSED        [ 52%]
tests/test_unit.py::test_verify_pgsql_skips_tables_when_db_missing PASSED [ 52%]
tests/test_unit.py::test_verify_mysql_databases PASSED                   [ 52%]
tests/test_unit.py::test_fmt_bytes[500-0.5 KB] PASSED                    [ 53%]
tests/test_unit.py::test_fmt_bytes[1024-1.0 KB] PASSED                   [ 53%]
tests/test_unit.py::test_fmt_bytes[1048576-1.0 MB] PASSED                [ 54%]
tests/test_unit.py::test_fmt_bytes[536870912-512.0 MB] PASSED            [ 54%]
tests/test_unit.py::test_fmt_bytes[1073741824-1.0 GB] PASSED             [ 55%]
tests/test_unit.py::test_fmt_bytes[2684354560.0-2.5 GB] PASSED           [ 55%]
tests/test_unit.py::test_validate_backup_path_allowed[/backup/s3] PASSED [ 56%]
tests/test_unit.py::test_validate_backup_path_allowed[/mnt/clouddump] PASSED [ 56%]
tests/test_unit.py::test_validate_backup_path_allowed[/tmp/test] PASSED  [ 57%]
tests/test_unit.py::test_validate_backup_path_rejected[/etc/passwd] PASSED [ 57%]
tests/test_unit.py::test_validate_backup_path_rejected[/root] PASSED     [ 58%]
tests/test_unit.py::test_validate_backup_path_rejected[/home/user] PASSED [ 58%]
tests/test_unit.py::test_validate_backup_path_rejected[/var/data] PASSED [ 58%]
tests/test_unit.py::test_format_job_config_redacts_secrets PASSED        [ 59%]
tests/test_unit.py::test_format_job_config_valid_json PASSED             [ 59%]
tests/test_unit.py::test_update_last_run_populates_state PASSED          [ 60%]
tests/test_unit.py::test_update_job_metric PASSED                        [ 60%]
tests/test_unit.py::test_update_job_metric_without_net PASSED            [ 61%]
tests/test_unit.py::test_update_last_run_initial_defaults PASSED         [ 61%]
tests/test_unit.py::test_healthz_returns_200 PASSED                      [ 62%]
tests/test_unit.py::test_healthz_404_on_other_paths PASSED               [ 62%]
tests/test_runners.py::TestS3Runner::test_basic_sync PASSED              [ 63%]
tests/test_runners.py::TestS3Runner::test_delete_disabled PASSED         [ 63%]
tests/test_runners.py::TestS3Runner::test_endpoint_url PASSED            [ 64%]
tests/test_runners.py::TestS3Runner::test_env_credentials PASSED         [ 64%]
tests/test_runners.py::TestS3Runner::test_missing_source PASSED          [ 64%]
tests/test_runners.py::TestS3Runner::test_invalid_source_prefix PASSED   [ 65%]
tests/test_runners.py::TestS3Runner::test_nonzero_exit PASSED            [ 65%]
tests/test_runners.py::TestS3Runner::test_creates_destination_dir PASSED [ 66%]
tests/test_runners.py::TestAzureRunner::test_basic_sync PASSED           [ 66%]
tests/test_runners.py::TestAzureRunner::test_delete_disabled PASSED      [ 67%]
tests/test_runners.py::TestAzureRunner::test_debug_adds_log_level_flag PASSED [ 67%]
tests/test_runners.py::TestAzureRunner::test_debug_writes_azcopy_sidecar_log PASSED [ 68%]
tests/test_runners.py::TestAzureRunner::test_debug_prunes_azcopy_source_after_copy PASSED [ 68%]
tests/test_runners.py::TestAzureRunner::test_prune_stale_azcopy_logs PASSED [ 69%]
tests/test_runners.py::TestAzureRunner::test_no_debug_flags_by_default PASSED [ 69%]
tests/test_runners.py::TestAzureRunner::test_missing_source PASSED       [ 70%]
tests/test_runners.py::TestAzureRunner::test_invalid_source_prefix PASSED [ 70%]
tests/test_runners.py::TestAzureRunner::test_nonzero_exit PASSED         [ 70%]
tests/test_runners.py::TestAzureRunner::test_creates_destination_dir PASSED [ 71%]
tests/test_runners.py::TestPgSQLRunner::test_custom_db_retries PASSED    [ 71%]
tests/test_runners.py::TestPgSQLRunner::test_default_db_retries PASSED   [ 72%]
tests/test_runners.py::TestPgSQLRunner::test_only_system_databases_returns_success PASSED [ 72%]
tests/test_runners.py::TestMySQLRunner::test_basic_dump PASSED           [ 73%]
tests/test_runners.py::TestMySQLRunner::test_env_password PASSED         [ 73%]
tests/test_runners.py::TestMySQLRunner::test_excludes_system_databases PASSED [ 74%]
tests/test_runners.py::TestMySQLRunner::test_explicit_databases PASSED   [ 74%]
tests/test_runners.py::TestMySQLRunner::test_missing_host PASSED         [ 75%]
tests/test_runners.py::TestMySQLRunner::test_missing_password PASSED     [ 75%]
tests/test_runners.py::TestMySQLRunner::test_nonzero_exit PASSED         [ 76%]
tests/test_runners.py::TestMySQLRunner::test_custom_db_retries PASSED    [ 76%]
tests/test_runners.py::TestMySQLRunner::test_creates_destination_dir PASSED [ 76%]
tests/test_runners.py::TestMySQLRunner::test_only_system_databases_returns_success PASSED [ 77%]
tests/test_runners.py::TestGitHubRunner::test_default_flags PASSED       [ 77%]
tests/test_runners.py::TestGitHubRunner::test_repositories_filter PASSED [ 78%]
tests/test_runners.py::TestGitHubRunner::test_repositories_default_all PASSED [ 78%]
tests/test_runners.py::TestGitHubRunner::test_repos_disabled PASSED      [ 79%]
tests/test_runners.py::TestGitHubRunner::test_issues_disabled PASSED     [ 79%]
tests/test_runners.py::TestGitHubRunner::test_pulls_disabled PASSED      [ 80%]
tests/test_runners.py::TestGitHubRunner::test_labels_disabled PASSED     [ 80%]
tests/test_runners.py::TestGitHubRunner::test_milestones_disabled PASSED [ 81%]
tests/test_runners.py::TestGitHubRunner::test_releases_disabled PASSED   [ 81%]
tests/test_runners.py::TestGitHubRunner::test_forks_enabled PASSED       [ 82%]
tests/test_runners.py::TestGitHubRunner::test_archived_disabled PASSED   [ 82%]
tests/test_runners.py::TestGitHubRunner::test_lfs_enabled PASSED         [ 82%]
tests/test_runners.py::TestGitHubRunner::test_everything_disabled PASSED [ 83%]
tests/test_runners.py::TestGitHubRunner::test_missing_name PASSED        [ 83%]
tests/test_runners.py::TestGitHubRunner::test_missing_destination PASSED [ 84%]
tests/test_runners.py::TestGitHubRunner::test_missing_token PASSED       [ 84%]
tests/test_runners.py::TestGitHubRunner::test_nonzero_exit PASSED        [ 85%]
tests/test_runners.py::TestGitHubRunner::test_creates_destination_dir PASSED [ 85%]
tests/test_runners.py::TestGitHubRunner::test_token_not_in_log PASSED    [ 86%]
tests/test_runners.py::TestRsyncRunner::test_basic_sync PASSED           [ 86%]
tests/test_runners.py::TestRsyncRunner::test_ssh_options PASSED          [ 87%]
tests/test_runners.py::TestRsyncRunner::test_default_ssh_port PASSED     [ 87%]
tests/test_runners.py::TestRsyncRunner::test_delete_disabled PASSED      [ 88%]
tests/test_runners.py::TestRsyncRunner::test_exclude_patterns PASSED     [ 88%]
tests/test_runners.py::TestRsyncRunner::test_missing_source PASSED       [ 88%]
tests/test_runners.py::TestRsyncRunner::test_invalid_source_no_colon PASSED [ 89%]
tests/test_runners.py::TestRsyncRunner::test_rejects_shell_metacharacters_in_source[$(whoami)@host:/path] PASSED [ 89%]
tests/test_runners.py::TestRsyncRunner::test_rejects_shell_metacharacters_in_source[user@host$(cmd):/path] PASSED [ 90%]
tests/test_runners.py::TestRsyncRunner::test_rejects_shell_metacharacters_in_source[user@host:/path;rm -rf /] PASSED [ 90%]
tests/test_runners.py::TestRsyncRunner::test_rejects_shell_metacharacters_in_source[user@host`id`:/path] PASSED [ 91%]
tests/test_runners.py::TestRsyncRunner::test_missing_ssh_key PASSED      [ 91%]
tests/test_runners.py::TestRsyncRunner::test_nonzero_exit PASSED         [ 92%]
tests/test_runners.py::TestRsyncRunner::test_creates_destination_dir PASSED [ 92%]
tests/test_runners.py::TestRsyncRunner::test_min_age_days_builds_filelist PASSED [ 93%]
tests/test_runners.py::TestRsyncRunner::test_min_age_days_no_delete PASSED [ 93%]
tests/test_runners.py::TestRsyncRunner::test_min_age_days_no_files_returns_0 PASSED [ 94%]
tests/test_runners.py::TestRsyncRunner::test_min_age_days_find_failure PASSED [ 94%]
tests/test_runners.py::TestRsyncRunner::test_min_age_days_cleans_up_tempfile PASSED [ 94%]
tests/test_runners.py::TestRsyncRunner::test_without_min_age_no_files_from PASSED [ 95%]
tests/test_runners.py::TestFindOldFiles::test_parses_list_and_filters_by_age PASSED [ 95%]
tests/test_runners.py::TestFindOldFiles::test_returns_none_on_rsync_failure PASSED [ 96%]
tests/test_runners.py::TestFindOldFiles::test_empty_list_returns_empty PASSED [ 96%]
tests/test_runners.py::TestFindOldFiles::test_uses_list_only_not_shell_find PASSED [ 97%]
tests/test_runners.py::TestJobDispatch::test_unknown_type_returns_1 PASSED [ 97%]
tests/test_runners.py::TestJobDispatch::test_empty_targets_returns_1 PASSED [ 98%]
tests/test_runners.py::TestJobDispatch::test_dispatches_to_s3 PASSED     [ 98%]
tests/test_runners.py::TestJobDispatch::test_dispatches_to_github PASSED [ 99%]
tests/test_runners.py::TestJobDispatch::test_multiple_targets_all_attempted PASSED [ 99%]
tests/test_runners.py::TestJobDispatch::test_multiple_targets_all_succeed PASSED [100%]

-- generated xml file: /home/runner/work/CloudDump/CloudDump/unit-results.xml --
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.12.13-final-0 _______________

Name                      Stmts   Miss  Cover   Missing
-------------------------------------------------------
clouddump/__init__.py       145     53    63%   91, 113-118, 123-125, 153-164, 233-284
clouddump/__main__.py       222    222     0%   3-334
clouddump/config.py         325     67    79%   33-41, 55-57, 61-63, 124-125, 169-171, 175-180, 196-199, 203-229, 252-255, 280, 293, 308, 332, 350, 359, 377, 404, 414, 422, 435, 441, 459, 462, 469, 487
clouddump/cron.py            22      0   100%
clouddump/email.py          100     87    13%   20, 35-95, 112-171
clouddump/health.py          37     10    73%   62, 67-75
clouddump/job_azure.py       72      5    93%   43-45, 68-69
clouddump/job_github.py      58      6    90%   60, 63, 66, 69, 72, 75
clouddump/job_mysql.py      111     29    74%   18-31, 62-63, 94, 110-111, 130, 135-146, 152-155
clouddump/job_pgsql.py      139     64    54%   38-57, 78-79, 88-89, 95-98, 101-102, 128, 130-132, 134-136, 151-156, 167-199, 202, 204
clouddump/job_rsync.py       94      5    95%   57-58, 115, 138-139
clouddump/job_s3.py          42      0   100%
clouddump/jobs.py            40      1    98%   35
-------------------------------------------------------
TOTAL                      1407    549    61%
Coverage JSON written to file coverage.json
============================= 217 passed in 2.82s ==============================
Integration test output
[2026-04-22 04:50:07] level=debug   [test-mysql] Using all databases except excluded and system ones
[2026-04-22 04:50:07] level=info    [test-mysql] Databases to backup: testdb1 testdb2
[2026-04-22 04:50:07] level=debug   [test-mysql] Processing database: testdb1
[2026-04-22 04:50:07] level=debug   [test-mysql] Running mysqldump of testdb1 (attempt 1/3)...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Connecting to mysql...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Starting transaction...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Setting savepoint...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Retrieving table structure for table users...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Sending SELECT query...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Retrieving rows...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Rolling back to savepoint sp...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Releasing savepoint...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Disconnecting from mysql...
[2026-04-22 04:50:07] level=info    [test-mysql] mysqldump completed
[2026-04-22 04:50:07] level=debug   [test-mysql] Compressing backupfile /backup/mysql/testdb1-20260422045007.sql...
[2026-04-22 04:50:07] level=debug   [test-mysql] Compression completed. Compressed file: /backup/mysql/testdb1-20260422045007.sql.bz2
[2026-04-22 04:50:07] level=debug   [test-mysql] Moving /backup/mysql/testdb1-20260422045007.sql.bz2 to /backup/mysql/testdb1.sql.bz2...
[2026-04-22 04:50:07] level=debug   [test-mysql] Backup completed successfully: /backup/mysql/testdb1.sql.bz2
[2026-04-22 04:50:07] level=debug   [test-mysql] Processing database: testdb2
[2026-04-22 04:50:07] level=debug   [test-mysql] Running mysqldump of testdb2 (attempt 1/3)...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Connecting to mysql...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Starting transaction...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Setting savepoint...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Retrieving table structure for table products...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Sending SELECT query...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Retrieving rows...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Rolling back to savepoint sp...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Releasing savepoint...
[2026-04-22 04:50:07] level=debug   [test-mysql]   -- Disconnecting from mysql...
[2026-04-22 04:50:07] level=info    [test-mysql] mysqldump completed
[2026-04-22 04:50:07] level=debug   [test-mysql] Compressing backupfile /backup/mysql/testdb2-20260422045007.sql...
[2026-04-22 04:50:07] level=debug   [test-mysql] Compression completed. Compressed file: /backup/mysql/testdb2-20260422045007.sql.bz2
[2026-04-22 04:50:07] level=debug   [test-mysql] Moving /backup/mysql/testdb2-20260422045007.sql.bz2 to /backup/mysql/testdb2.sql.bz2...
[2026-04-22 04:50:07] level=debug   [test-mysql] Backup completed successfully: /backup/mysql/testdb2.sql.bz2
[2026-04-22 04:50:07] level=info    [test-mysql] Total dump size: 4.3 KB
[2026-04-22 04:50:07] level=info    [test-mysql] All 2 database(s) backed up successfully.
[2026-04-22 04:50:07] level=info    [test-mysql] Attempt completed successfully
[2026-04-22 04:50:07] level=info    [test-mysql] Job report: Success | test-mysql (mysql) | 0m 0s | attempt 1/1
[2026-04-22 04:50:07] level=info    [test-mysql] Sending email to verify@clouddump.local from test@clouddump.local.
[2026-04-22 04:50:07] level=info    All jobs completed
--- end logs ---

=== Verification ===

  Container:
  PASS  CloudDump is still running

  S3 sync:
  PASS  file1.txt exists
  PASS  file1.txt has expected content
  PASS  subdir/file2.txt exists
  PASS  subdir/nested/file3.txt exists

  PostgreSQL dump:
  PASS  testuser dump exists and non-empty
  PASS  testdb1 dump exists and non-empty
  PASS  testdb2 dump exists and non-empty

  MySQL dump:
  PASS  testdb1 dump exists and non-empty
  PASS  testdb2 dump exists and non-empty

  Email (SMTP via Mailpit):
  PASS  at least one email received by Mailpit

[6/6] Running tool smoke tests...

  Bundled tools (in Docker image):
  PASS  github-backup installed
  PASS  git installed
  PASS  aws CLI installed
  PASS  azcopy installed
  PASS  pg_dump installed
  PASS  psql installed

=== Results: 17 passed, 0 failed ===
All checks passed.

Cleaning up...
clouddump-integration-test

Last run: 2026-04-22 04:50:28 UTC | Commit: 490b225 | Action run

@ralftar ralftar closed this Apr 22, 2026
@ralftar ralftar deleted the ralftar/pgsql-tcp-keepalives branch April 22, 2026 04:47
@ralftar ralftar restored the ralftar/pgsql-tcp-keepalives branch April 22, 2026 04:48
@ralftar ralftar reopened this Apr 22, 2026
@ralftar ralftar merged commit fdd4097 into main Apr 22, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant