|
18 | 18 | sleep_until_postgrest_full_reload, |
19 | 19 | sleep_until_postgrest_scache_reload, |
20 | 20 | wait_until_exit, |
| 21 | + wait_until_status_code, |
21 | 22 | ) |
22 | 23 |
|
23 | 24 |
|
@@ -105,6 +106,88 @@ def sleep(): |
105 | 106 | t.join() |
106 | 107 |
|
107 | 108 |
|
| 109 | +def test_pool_flushes_metric(defaultenv): |
| 110 | + "Should increase pool flushes metric when the pool is flushed" |
| 111 | + |
| 112 | + with run(env=defaultenv, port=freeport()) as postgrest: |
| 113 | + response = postgrest.admin.get("/metrics") |
| 114 | + assert response.status_code == 200 |
| 115 | + before = float( |
| 116 | + re.search(r"pgrst_db_pool_flushes_total ([0-9.e+-]+)", response.text).group( |
| 117 | + 1 |
| 118 | + ) |
| 119 | + ) |
| 120 | + |
| 121 | + postgrest.process.send_signal(signal.SIGUSR1) |
| 122 | + sleep_until_postgrest_scache_reload() |
| 123 | + |
| 124 | + response = postgrest.admin.get("/metrics") |
| 125 | + assert response.status_code == 200 |
| 126 | + after = float( |
| 127 | + re.search(r"pgrst_db_pool_flushes_total ([0-9.e+-]+)", response.text).group( |
| 128 | + 1 |
| 129 | + ) |
| 130 | + ) |
| 131 | + assert after == before + 1 |
| 132 | + |
| 133 | + |
| 134 | +def test_pool_flushes_metric_with_schema_cache_retries(defaultenv, metapostgrest): |
| 135 | + "Should flush the pool exactly once even when schema cache reload retries" |
| 136 | + |
| 137 | + role = "timeout_authenticator" |
| 138 | + app_name = "pool-flush-retry" |
| 139 | + env = { |
| 140 | + **defaultenv, |
| 141 | + "PGUSER": role, |
| 142 | + "PGAPPNAME": app_name, |
| 143 | + "PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "50", |
| 144 | + } |
| 145 | + |
| 146 | + with run(env=env, port=freeport()) as postgrest: |
| 147 | + response = postgrest.admin.get("/metrics") |
| 148 | + assert response.status_code == 200 |
| 149 | + before = float( |
| 150 | + re.search(r"pgrst_db_pool_flushes_total ([0-9.e+-]+)", response.text).group( |
| 151 | + 1 |
| 152 | + ) |
| 153 | + ) |
| 154 | + |
| 155 | + try: |
| 156 | + # Force schema cache reload failures to trigger retries. |
| 157 | + set_statement_timeout(metapostgrest, role, 20) |
| 158 | + postgrest.process.send_signal(signal.SIGUSR1) |
| 159 | + |
| 160 | + postgrest.wait_until_scache_starts_loading(max_seconds=2) |
| 161 | + |
| 162 | + # Give retry loop time to run and verify it doesn't flush the pool. |
| 163 | + time.sleep(1) |
| 164 | + |
| 165 | + response = postgrest.admin.get("/metrics") |
| 166 | + assert response.status_code == 200 |
| 167 | + during_retries = float( |
| 168 | + re.search( |
| 169 | + r"pgrst_db_pool_flushes_total ([0-9.e+-]+)", response.text |
| 170 | + ).group(1) |
| 171 | + ) |
| 172 | + assert during_retries == before |
| 173 | + |
| 174 | + reset_statement_timeout(metapostgrest, role) |
| 175 | + # Ensure next retry establishes fresh sessions with the reset timeout. |
| 176 | + metapostgrest.session.get(f"/rpc/terminate_pgrst?appname={app_name}") |
| 177 | + wait_until_status_code(postgrest.admin.baseurl + "/ready", 12, 200) |
| 178 | + finally: |
| 179 | + reset_statement_timeout(metapostgrest, role) |
| 180 | + |
| 181 | + response = postgrest.admin.get("/metrics") |
| 182 | + assert response.status_code == 200 |
| 183 | + after = float( |
| 184 | + re.search(r"pgrst_db_pool_flushes_total ([0-9.e+-]+)", response.text).group( |
| 185 | + 1 |
| 186 | + ) |
| 187 | + ) |
| 188 | + assert after == before + 1 |
| 189 | + |
| 190 | + |
108 | 191 | def test_random_port_bound(defaultenv): |
109 | 192 | "PostgREST should bind to a random port when PGRST_SERVER_PORT is 0." |
110 | 193 |
|
@@ -661,55 +744,53 @@ def test_log_level(level, defaultenv): |
661 | 744 | response = postgrest.session.get("/") |
662 | 745 | assert response.status_code == 200 |
663 | 746 |
|
664 | | - output = sorted(postgrest.read_stdout(nlines=7)) |
| 747 | + output = postgrest.read_stdout(nlines=9) |
| 748 | + |
| 749 | + def match_log(matchers): |
| 750 | + ito = iter(output) |
| 751 | + itm = iter(matchers) |
| 752 | + nextMatcher = next(itm, None) |
| 753 | + while nextMatcher is not None and (line := next(ito, None)) is not None: |
| 754 | + if re.match(nextMatcher, line) is not None: |
| 755 | + nextMatcher = next(itm, None) |
| 756 | + if nextMatcher is not None: |
| 757 | + raise AssertionError( |
| 758 | + f"Expected log line matching {nextMatcher} not found in output" |
| 759 | + ) |
665 | 760 |
|
666 | 761 | if level == "crit": |
667 | 762 | assert len(output) == 0 |
668 | 763 | elif level == "error": |
669 | | - assert re.match( |
670 | | - r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"', |
671 | | - output[0], |
| 764 | + match_log( |
| 765 | + [r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"'] |
672 | 766 | ) |
673 | 767 | assert len(output) == 1 |
674 | 768 | elif level == "warn": |
675 | | - assert re.match( |
676 | | - r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"', |
677 | | - output[0], |
678 | | - ) |
679 | | - assert re.match( |
680 | | - r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"', |
681 | | - output[1], |
| 769 | + match_log( |
| 770 | + [ |
| 771 | + r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"', |
| 772 | + r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"', |
| 773 | + ] |
682 | 774 | ) |
683 | 775 | assert len(output) == 2 |
684 | 776 | elif level == "info": |
685 | | - assert re.match( |
686 | | - r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"', |
687 | | - output[0], |
688 | | - ) |
689 | | - assert re.match( |
690 | | - r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"', |
691 | | - output[1], |
692 | | - ) |
693 | | - assert re.match( |
694 | | - r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"', |
695 | | - output[2], |
| 777 | + match_log( |
| 778 | + [ |
| 779 | + r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"', |
| 780 | + r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"', |
| 781 | + r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"', |
| 782 | + ] |
696 | 783 | ) |
697 | 784 | assert len(output) == 3 |
698 | 785 | elif level == "debug": |
699 | | - assert re.match( |
700 | | - r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"', |
701 | | - output[0], |
| 786 | + match_log( |
| 787 | + [ |
| 788 | + r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"', |
| 789 | + r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"', |
| 790 | + r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"', |
| 791 | + ] |
702 | 792 | ) |
703 | | - assert re.match( |
704 | | - r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"', |
705 | | - output[1], |
706 | | - ) |
707 | | - assert re.match( |
708 | | - r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"', |
709 | | - output[2], |
710 | | - ) |
711 | | - |
712 | | - assert len(output) == 7 |
| 793 | + assert len(output) == 9 |
713 | 794 | assert any("Connection" and "is available" in line for line in output) |
714 | 795 | assert any("Connection" and "is used" in line for line in output) |
715 | 796 |
|
@@ -1346,16 +1427,16 @@ def test_db_error_logging_to_stderr(level, defaultenv, metapostgrest): |
1346 | 1427 | assert response.status_code == 500 |
1347 | 1428 |
|
1348 | 1429 | # ensure the message appears on the logs |
1349 | | - output = sorted(postgrest.read_stdout(nlines=6)) |
| 1430 | + output = postgrest.read_stdout(nlines=8) |
1350 | 1431 |
|
1351 | 1432 | if level == "crit": |
1352 | 1433 | assert len(output) == 0 |
1353 | 1434 | elif level == "debug": |
1354 | | - assert " 500 " in output[0] |
1355 | | - assert "canceling statement due to statement timeout" in output[5] |
| 1435 | + assert " 500 " in output[7] |
| 1436 | + assert "canceling statement due to statement timeout" in output[6] |
1356 | 1437 | else: |
1357 | | - assert " 500 " in output[0] |
1358 | | - assert "canceling statement due to statement timeout" in output[1] |
| 1438 | + assert " 500 " in output[1] |
| 1439 | + assert "canceling statement due to statement timeout" in output[0] |
1359 | 1440 |
|
1360 | 1441 | reset_statement_timeout(metapostgrest, role) |
1361 | 1442 |
|
@@ -1460,6 +1541,7 @@ def test_admin_metrics(defaultenv): |
1460 | 1541 | assert "pgrst_db_pool_waiting" in response.text |
1461 | 1542 | assert "pgrst_db_pool_available" in response.text |
1462 | 1543 | assert "pgrst_db_pool_timeouts_total" in response.text |
| 1544 | + assert "pgrst_db_pool_flushes_total" in response.text |
1463 | 1545 |
|
1464 | 1546 |
|
1465 | 1547 | def test_schema_cache_startup_load_with_in_db_config(defaultenv, metapostgrest): |
@@ -1557,10 +1639,10 @@ def test_log_pool_req_observation(level, defaultenv): |
1557 | 1639 | postgrest.session.get("/authors_only", headers=headers) |
1558 | 1640 |
|
1559 | 1641 | if level == "debug": |
1560 | | - output = postgrest.read_stdout(nlines=5) |
| 1642 | + output = postgrest.read_stdout(nlines=7) |
| 1643 | + assert len(output) == 7 |
1561 | 1644 | assert pool_req in output[1] |
1562 | | - assert pool_req_fullfill in output[4] |
1563 | | - assert len(output) == 5 |
| 1645 | + assert pool_req_fullfill in output[6] |
1564 | 1646 | elif level == "info": |
1565 | 1647 | output = postgrest.read_stdout(nlines=4) |
1566 | 1648 | assert len(output) == 1 |
|
0 commit comments