Skip to content

Commit f80e496

Browse files
author
Gurov Oleg
committed
feat: release v0.7.0 with corrected workloads and direct SQL transport
Use direct database connections with SSH reserved for host operations and metrics. Correct Pagila and IMDb data distributions, temporal semantics, query aggregates and selective indexes, and document profile size estimates. Return newly created Pagila rentals through the open-inventory index and serialize inventory moves with rental creation using a fresh snapshot after the row lock. Protect JSON strings during legacy lshw structural recovery. Add regression coverage for rental reuse, concurrent moves and JSON values. Validation: 363 unit tests and 10 PostgreSQL 18.6 Pagila integration tests passed; Ruff, wheel/sdist metadata and documentation checks, and installed wheel validation passed. Chart tests require Node.js and were skipped locally.
1 parent 5eec23c commit f80e496

67 files changed

Lines changed: 2319 additions & 732 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# pg-perf-bench
22

3+
For this benchmark stand, see the [source synchronization workflow](doc/sidecar-source-workflow.md).
4+
35
## Overview
46

57
Based on [pg_perfbench](https://github.com/TantorLabs/pg_perfbench).
@@ -65,7 +67,7 @@ selected target. The workload commands themselves run on the machine where
6567
|---|---|---|
6668
| `local` | local machine | local machine |
6769
| `docker` | existing container | local machine through a published port |
68-
| `ssh` | remote host | local machine through an SSH local-forwarding port |
70+
| `ssh` | remote host | local machine, direct TCP to PostgreSQL or a pooler |
6971
| `--managed` | PostgreSQL protocol only; no host transport | local machine through the managed endpoint |
7072

7173
This separation keeps workload generation independent of target management and
@@ -229,6 +231,12 @@ collects host inventory without sudo and keeps the target container's
229231
`pg_config` evidence separate. Raw interface state is retained, but runtime
230232
Docker bridges do not participate in the stable JOIN hardware identity.
231233

234+
For legacy `lshw 02.18.x` (including Debian 11), hardware tables recover the
235+
known malformed `-class ... -json` output using the same narrow repairs as
236+
pg-diag. The fallback runs only after strict JSON parsing fails and only for
237+
`lshw_*.sh` collectors. An absent hardware class reported as `]` becomes an
238+
empty table; unrecoverable output still produces a collection error.
239+
232240
## Running a benchmark
233241

234242
Exactly one iteration axis is required:
@@ -701,10 +709,8 @@ The workload reaches PostgreSQL through the port published on `--host` and
701709
--ssh-user postgres \
702710
--ssh-key /secure/path/id_ed25519 \
703711
--ssh-known-hosts /secure/path/known_hosts \
704-
--remote-pg-host 127.0.0.1 \
705-
--remote-pg-port 5432 \
706-
--host 127.0.0.1 \
707-
--port 55432
712+
--host db-host.example \
713+
--port 5432
708714
```
709715

710716
To use an identity already loaded into a local agent, replace `--ssh-key` with
@@ -721,10 +727,8 @@ pg-perf-bench collect-all-info \
721727
--ssh-user postgres \
722728
--ssh-agent \
723729
--ssh-known-hosts /secure/path/known_hosts \
724-
--remote-pg-host 127.0.0.1 \
725-
--remote-pg-port 5432 \
726-
--host 127.0.0.1 \
727-
--port 55432 \
730+
--host db-host.example \
731+
--port 5432 \
728732
--database appdb \
729733
--pg-bin-path /usr/lib/postgresql/18/bin
730734
```
@@ -736,11 +740,15 @@ agent, run `ssh-add`, or forward the agent to the remote host. Both modes use
736740
explicit public-key authentication and the selected `known_hosts` policy
737741
without loading the user's OpenSSH configuration.
738742

739-
For database modes, `--host` and `--port` are the local bind address and
740-
free port. `--remote-pg-host` and `--remote-pg-port` identify PostgreSQL from
741-
the SSH server. Host commands run remotely; `asyncpg`, `pgbench`, and `psql`
742-
connect through native AsyncSSH local forwarding. No `AcceptEnv` change is
743-
required on the SSH server.
743+
For database modes, `--host` and `--port` are the PostgreSQL or pooler endpoint
744+
reachable directly from the load-generator host. `asyncpg`, `pgbench`, and `psql`
745+
connect to this endpoint directly. `--ssh-host` selects the host for OS metrics,
746+
host commands and lifecycle operations; it can differ from the SQL endpoint.
747+
AsyncSSH does not forward database traffic. No `AcceptEnv` change is required.
748+
749+
Older commands using `--remote-pg-host` or `--remote-pg-port` fail with a migration
750+
message before connecting. Remove those options and replace the former local
751+
bind address/port with the directly reachable database endpoint.
744752

745753
## Report contents
746754

‎doc/sidecar-source-workflow.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Working from source on a benchmark sidecar
2+
3+
Keep one authoritative Git checkout, make and review changes there, then synchronize
4+
it to the load-generator host. SQL connects directly to PostgreSQL or its session
5+
pool through `--host` and `--port`; SSH is used for host operations and OS metrics.
6+
7+
The repository helper `scripts/sync_sidecar.py` mirrors the checkout to
8+
`~/pg_perf_bench` on the selected sidecar. Supply the destination and SSH key
9+
explicitly when using it on your own infrastructure:
10+
11+
```bash
12+
python3 scripts/sync_sidecar.py --host bench@sidecar.example --key /path/to/key --dry-run
13+
python3 scripts/sync_sidecar.py --host bench@sidecar.example --key /path/to/key
14+
```
15+
16+
The helper expects an existing `~/pgpb-venv` Python 3.12 environment with the
17+
project dependencies and CLI installed. Dependency changes require a separate
18+
environment update; synchronization does not install dependencies.
19+
20+
Do not synchronize while a benchmark is active. The helper checks for an active
21+
CLI benchmark before changing files. A dry run only previews the rsync changes.
22+
Before a real synchronization it backs up the remote source and source-path
23+
configuration under `~/bench/pgpb_source_sync_<UTC timestamp>/`.
24+
25+
The mirror includes removal of files that no longer exist in the checkout.
26+
Git data, virtual environments, caches, local `.env` files and build/runtime
27+
outputs are excluded. Benchmark results under `~/bench` are outside the mirror.
28+
29+
The existing `~/pgpb-venv/bin/pg-perf-bench` command imports directly from
30+
`~/pg_perf_bench/src` through the environment's `pg_perf_bench_source.pth` file.
31+
SQL, templates and other resources are read from that source tree. No wheel build
32+
or package reinstall is required. The helper checks the resulting import path
33+
and runs `pg-perf-bench validate`; start a fresh process to use the updated code.
34+
35+
To roll back, restore the saved `source/` directory and `.pth` configuration
36+
identified by the selected backup's `state.json`. If `pth_existed` is false,
37+
remove only `pg_perf_bench_source.pth` to return to the installed package.

‎doc/ssh_mode_usage.md‎

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
Use SSH transport when PostgreSQL and host fact collection run on a remote
44
server while `pg_perf_bench`, `pgbench`, and `psql` run locally. The transport
5-
uses AsyncSSH and native local port forwarding; `sshtunnel` and server-side
5+
uses AsyncSSH for remote host operations. SQL connects directly over TCP from
6+
the load-generator host; there is no database port forwarding. Server-side
67
`AcceptEnv` changes are not required.
78

89
## Execution boundary
910

1011
| Operation | Location |
1112
|---|---|
12-
| PostgreSQL connection | local forwarded port |
13+
| PostgreSQL connection | direct TCP from the load generator to `--host:--port` |
1314
| `pgbench` and `psql` | local workload-generator host |
1415
| Host fact collectors | remote host |
1516
| Timed `pg_diag` OS sampler | remote host |
@@ -50,14 +51,23 @@ disposable stands.
5051

5152
## Address model
5253

53-
SSH database modes use two address pairs:
54+
SSH mode separates the SQL endpoint from the host-management endpoint:
5455

55-
- `--host` and `--port` define a free local bind address and port;
56-
- `--remote-pg-host` and `--remote-pg-port` identify PostgreSQL as seen from
57-
the SSH server.
56+
- `--host` and `--port` identify PostgreSQL or a pooler reachable directly from
57+
the machine running `pg-perf-bench`;
58+
- `--ssh-host` and `--ssh-port` identify the database host used for OS metrics,
59+
fact collection and lifecycle operations.
5860

59-
`asyncpg`, `pgbench`, and `psql` connect to the local pair. AsyncSSH forwards
60-
that traffic to the remote pair. The local port must not already be in use.
61+
`asyncpg`, `pgbench`, and `psql` use the SQL endpoint directly. This keeps SSH
62+
forwarding and encryption out of the measured database traffic. The endpoints
63+
can differ, for example when SQL goes through HAProxy/PgBouncer while metrics
64+
are collected on the primary. Select the SSH host corresponding to the database
65+
server whose OS metrics you intend to measure.
66+
67+
The former `--remote-pg-host` and `--remote-pg-port` options are rejected before
68+
any connection is made. To migrate, remove both options and replace the old
69+
local bind address and port with the SQL endpoint reachable from the generator.
70+
A working SSH login alone does not establish database network access.
6171

6272
## Read-only collection
6373

@@ -68,18 +78,16 @@ PGPASSWORD=secret pg-perf-bench collect-all-info \
6878
--ssh-user postgres \
6979
--ssh-key ~/.ssh/pg_perf_bench \
7080
--ssh-known-hosts ~/.ssh/known_hosts \
71-
--remote-pg-host 127.0.0.1 \
72-
--remote-pg-port 5432 \
73-
--host 127.0.0.1 \
74-
--port 55432 \
81+
--host db-host.example \
82+
--port 5432 \
7583
--user postgres \
7684
--database postgres \
7785
--pg-bin-path /usr/lib/postgresql/18/bin \
7886
--report-name ssh-facts
7987
```
8088

81-
Collection opens the tunnel and runs remote fact commands, but does not stop
82-
PostgreSQL or install a configuration file.
89+
Collection opens a direct SQL connection and runs host fact commands over SSH.
90+
It does not stop PostgreSQL or install a configuration file.
8391

8492
## Benchmark
8593

@@ -91,10 +99,8 @@ PGPASSWORD=secret pg-perf-bench benchmark \
9199
--ssh-user postgres \
92100
--ssh-key ~/.ssh/pg_perf_bench \
93101
--ssh-known-hosts ~/.ssh/known_hosts \
94-
--remote-pg-host 127.0.0.1 \
95-
--remote-pg-port 5432 \
96-
--host 127.0.0.1 \
97-
--port 55432 \
102+
--host db-host.example \
103+
--port 5432 \
98104
--user postgres \
99105
--database pg_perf_bench_test \
100106
--pg-data-path /var/lib/postgresql/18/main \
@@ -120,8 +126,7 @@ and use a bundled profile instead:
120126
PGPASSWORD=secret pg-perf-bench benchmark \
121127
--connection-type ssh --ssh-host db-host.example --ssh-user postgres \
122128
--ssh-key ~/.ssh/pg_perf_bench --ssh-known-hosts ~/.ssh/known_hosts \
123-
--remote-pg-host 127.0.0.1 --remote-pg-port 5432 \
124-
--host 127.0.0.1 --port 55432 --user postgres \
129+
--host db-host.example --port 5432 --user postgres \
125130
--pg-data-path /var/lib/postgresql/18/main \
126131
--pg-bin-path /usr/lib/postgresql/18/bin \
127132
--database pg_perf_bench_test --allow-database-reset --reset-mode schema \
@@ -163,7 +168,7 @@ and atomically renamed to the remote cluster's `postgresql.conf` before the rese
163168
sudo rule. Hardware collectors use `sudo -n` and fail fast when permission is
164169
not available.
165170

166-
The load commands always execute locally through the tunnel. The newest local
171+
The load commands always execute locally using the direct SQL endpoint. The newest local
167172
pgbench is selected automatically even when the remote server is PostgreSQL
168173
10–18. Concurrent CPU, RAM, disk, and network sampling executes on the remote
169174
database host through the already authenticated SSH session.
@@ -176,7 +181,6 @@ database host through the already authenticated SSH session.
176181
permissions, and the installed public key;
177182
- authentication failure in agent mode: verify `SSH_AUTH_SOCK`, `ssh-add -l`,
178183
the agent lifetime, and the installed public key;
179-
- local bind failure: choose an unused `--port`;
180-
- PostgreSQL connection failure with working SSH: verify the remote address,
181-
PostgreSQL authentication, and listen rules;
184+
- PostgreSQL connection failure with working SSH: verify reachability of
185+
`--host:--port` from the generator, PostgreSQL authentication and listen rules;
182186
- lifecycle failure: ensure `--ssh-user` owns or can control the cluster.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pg-perf-bench"
7-
version = "0.6.1"
7+
version = "0.7.0"
88
description = "Reproducible PostgreSQL TPS benchmarks with environment evidence and report comparison"
99
readme = "README.md"
1010
requires-python = ">=3.10"

‎scripts/sync_sidecar.py‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
"""Sync this source checkout to the benchmark sidecar without building a package."""
3+
4+
import argparse
5+
import shlex
6+
import subprocess
7+
from pathlib import Path
8+
9+
10+
def main():
11+
parser = argparse.ArgumentParser(description=__doc__)
12+
parser.add_argument('--host', default='psql-admin@10.234.9.110')
13+
parser.add_argument('--key', type=Path, default=Path('/home/oleg/Desktop/access/id_ed25519'))
14+
parser.add_argument('--dry-run', action='store_true')
15+
args = parser.parse_args()
16+
if args.host.startswith('-') or any(c.isspace() for c in args.host):
17+
parser.error('host must be a single SSH destination')
18+
source = Path(__file__).resolve().parents[1]
19+
assert (source / 'src/pg_perf_bench/cli.py').is_file()
20+
assert (source / 'pyproject.toml').is_file()
21+
ssh = ['ssh', '-i', str(args.key), '-o', 'BatchMode=yes']
22+
23+
def remote(code):
24+
result = subprocess.run(
25+
[*ssh, args.host, '~/pgpb-venv/bin/python -'],
26+
input=code,
27+
text=True,
28+
capture_output=True,
29+
check=True,
30+
)
31+
print(result.stdout, end='', flush=True)
32+
return result.stdout
33+
34+
if not args.dry_run:
35+
remote("""from pathlib import Path
36+
import datetime, json, shutil
37+
h=Path.home()
38+
for proc in Path('/proc').glob('[0-9]*/cmdline'):
39+
try:
40+
argv=proc.read_bytes().decode(errors='replace').split('\\0')
41+
except (OSError, ProcessLookupError):
42+
continue
43+
if 'benchmark' in argv and any(Path(a).name in ('pg-perf-bench','pg_perf_bench') for a in argv):
44+
raise SystemExit('A benchmark is running; sync after it completes.')
45+
backup=h/'bench'/('pgpb_source_sync_'+datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S%fZ'))
46+
backup.mkdir(parents=True)
47+
source=h/'pg_perf_bench'
48+
if source.exists(): shutil.copytree(source,backup/'source',symlinks=True)
49+
pth=h/'pgpb-venv/lib/python3.12/site-packages/pg_perf_bench_source.pth'
50+
if pth.exists(): shutil.copy2(pth,backup/pth.name)
51+
(backup/'state.json').write_text(json.dumps({'source':str(source),'pth':str(pth),'pth_existed':pth.exists()},indent=2)+'\\n')
52+
print('Backup:',backup)
53+
""")
54+
cmd = ['rsync', '-a', '--checksum', '--delete-delay', '--itemize-changes']
55+
for pattern in (
56+
'.git',
57+
'__pycache__',
58+
'*.py[cod]',
59+
'*.egg-info',
60+
'.pytest_cache',
61+
'.ruff_cache',
62+
'/.venv/',
63+
'/venv/',
64+
'/build/',
65+
'/dist/',
66+
'/log/',
67+
'/report/',
68+
'/db_logs/',
69+
'/.env',
70+
'/.env.*',
71+
):
72+
cmd.extend(['--exclude', pattern])
73+
if args.dry_run:
74+
cmd.append('--dry-run')
75+
cmd.extend(['-e', shlex.join(ssh), '--', str(source) + '/', args.host + ':pg_perf_bench/'])
76+
subprocess.run(cmd, check=True)
77+
if args.dry_run:
78+
return
79+
remote("""from pathlib import Path
80+
import json, subprocess
81+
h=Path.home();source=h/'pg_perf_bench/src'
82+
pth=h/'pgpb-venv/lib/python3.12/site-packages/pg_perf_bench_source.pth'
83+
pth.write_text('import sys; sys.path.insert(0, '+repr(str(source))+')\\n')
84+
code=("import pg_perf_bench; from pathlib import Path; "
85+
"p=Path(pg_perf_bench.__file__).resolve(); "
86+
"assert p == Path.home()/'pg_perf_bench/src/pg_perf_bench/__init__.py', p; "
87+
"print('Active source:',p)")
88+
subprocess.run([str(h/'pgpb-venv/bin/python'),'-c',code],cwd='/tmp',check=True)
89+
subprocess.run([str(h/'pgpb-venv/bin/pg-perf-bench'),'validate'],cwd='/tmp',check=True)
90+
""")
91+
print('Source synchronized; existing venv dependencies retained. No pip build/install.')
92+
93+
94+
if __name__ == '__main__':
95+
main()

‎src/pg_perf_bench/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""PostgreSQL performance benchmark and evidence collector."""
22

3-
__version__ = '0.6.1'
3+
__version__ = '0.7.0'

‎src/pg_perf_bench/cli.py‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def _add_host_args(parser: argparse.ArgumentParser, *, required: bool = True) ->
110110
parser.add_argument('--pg-data-path')
111111
parser.add_argument('--pg-bin-path')
112112
parser.add_argument('--container-name')
113-
parser.add_argument('--ssh-host')
113+
parser.add_argument('--ssh-host', help='remote host for OS metrics and host operations')
114114
parser.add_argument('--ssh-port', type=positive_int, default=22)
115115
parser.add_argument('--ssh-user', default='postgres')
116116
ssh_auth = parser.add_mutually_exclusive_group()
@@ -122,17 +122,28 @@ def _add_host_args(parser: argparse.ArgumentParser, *, required: bool = True) ->
122122
)
123123
parser.add_argument('--ssh-known-hosts')
124124
parser.add_argument('--ssh-insecure-no-host-key-check', action='store_true')
125-
parser.add_argument('--remote-pg-host')
126-
parser.add_argument('--remote-pg-port', type=positive_int)
125+
parser.add_argument('--remote-pg-host', help=argparse.SUPPRESS)
126+
parser.add_argument('--remote-pg-port', type=positive_int, help=argparse.SUPPRESS)
127127

128128

129129
def _add_database_args(
130130
parser: argparse.ArgumentParser,
131131
*,
132132
include_custom_config: bool = False,
133133
) -> None:
134-
parser.add_argument('--host', '--pg-host', dest='pg_host')
135-
parser.add_argument('--port', '--pg-port', dest='pg_port', type=positive_int)
134+
parser.add_argument(
135+
'--host',
136+
'--pg-host',
137+
dest='pg_host',
138+
help='PostgreSQL or pooler host reachable directly from the load generator',
139+
)
140+
parser.add_argument(
141+
'--port',
142+
'--pg-port',
143+
dest='pg_port',
144+
type=positive_int,
145+
help='direct PostgreSQL or pooler TCP port',
146+
)
136147
parser.add_argument('--user', '--pg-user', dest='pg_user', default='postgres')
137148
parser.add_argument(
138149
'--password',

0 commit comments

Comments
 (0)