Try It: the test_invoice_pg bench
This is the environment the bridge was actually exercised on — a real GenroPy site with a
real PostgreSQL database and 332 user accounts, 32 of which share a known password so a load
harness (or a curious developer with several browser profiles) can log in as many different people.
Everything below is checked out from repositories you already have, except the database itself.
1. The site: it ships inside GenroPy
The instance lives in the genropy repository and is tracked there —
nothing to write by hand:
genropy/projects/test_invoice/
├── instances/
│ ├── test_invoice/ # the SQLite variant
│ └── test_invoice_pg/ # the PostgreSQL one — this is the bench
│ ├── instanceconfig.xml
│ ├── root.py
│ └── site/
└── packages/ # invc, adm, sys …
instanceconfig.xml points at a PostgreSQL database named
test_invoice_pg on localhost, and it is the
only one of those files the repository actually tracks.
The site folder is not versioned. gnrasgiserve resolves
a site, not an instance (PathResolver.site_name_to_path), and it
looks in two places, in this order: <projects>/*/sites/<name>/
first, then <projects>/*/instances/<name>/ — but the second only
works if that folder holds a root.py, in which case the
site/ subfolder is created for you. Neither the site folder nor that
root.py is in the repository, so a fresh clone dies with
EntityNotFoundException: site test_invoice_pg not found and 77 tests
exclude themselves. Copy a root.py into
instances/test_invoice_pg/ and a siteconfig.xml
(declaring mainpackage="invc" and dojo version="11")
into its site/ — the ones from
sites/invoice_demo/ are identical in substance.
2. The database: ask for a dump
The populated database is not in any repository — 35 MB of data, 332
rows in adm.adm_user. It travels as a gzipped plain-SQL
pg_dump (~1.4 MB), which loads on any PostgreSQL version:
createdb test_invoice_pg
gunzip -c test_invoice_pg.sql.gz | psql -q -d test_invoice_pg
Why plain SQL and not the custom format. The dump was taken with PostgreSQL
17.7, and pg_restore refuses an archive written by a version newer than
itself — on 16 a custom-format file will not open at all. The SQL file is text and carries no
such limit. It has also been stripped of everything that would tie it to that machine or that
version: no OWNER TO, no GRANT, and none of
\restrict, \unrestrict or
SET transaction_timeout — the last one exists only from 17 onwards.
Verified by loading it into an empty database with ON_ERROR_STOP=1: no
errors, same 332 users, same 75 tables, same password hashes.
The data is the bench database as it stands, so the 32 accounts already carry the shared
password — you can log in straight after loading, without touching the fixtures in the next
step.
3. The accounts: 32 users, one password
The accounts come with the instance, but with passwords nobody knows. The genropy-asgi repository
carries the fixture that makes them usable, in benchmarks/test_users.zip.
The handed-over dump already has it applied — run this only if you rebuilt the database from
somewhere else:
cd benchmarks
unzip -o test_users.zip
psql -d test_invoice_pg -f set_pwd_a.sql
| File | What it is |
set_pwd_a.sql | 32 UPDATE statements in one transaction, one per name in usernames.txt, setting every password to the single letter a. This is the one you run. |
adm_user_backup_….sql | The pg_dump of adm.adm_user taken immediately before that overwrite, so the original passwords can be restored. It recreates the table from scratch — restoring means dropping adm.adm_user first, never replaying it onto a live table. |
usernames.txt | The 32 names the harness logs in as: amelia.martin, james.wilson, grace.hall, … |
usernames_all.txt | All 332 accounts in the database, for a wider run. |
They are fictional accounts on a throwaway database: invented names,
@testinvoice.com addresses, and the hash of one letter as a password. Fine
for a bench, obviously not for anything reachable from outside your machine.
4. Run it — single process
gnrasgiserve test_invoice_pg -p 8098 --nodebug
Open http://127.0.0.1:8098/index and log in as any name from
usernames.txt with password a. It is the same site
you would get from gnrwsgiserve — that is the point.
5. Run it — a pool of two workers
gnrasgiserve test_invoice_pg -p 8098 --workers 2 --nodebug
Now the front supervises two worker subprocesses and they announce themselves over a unix-domain
socket. In the log you will see one line per worker:
INFO: W:b7f498d8…: serving on uds:/…/gnrhub_…/hub.sock (pid 17782)
INFO: W:811bb8c2…: serving on uds:/…/gnrhub_…/hub.sock (pid 17781)
The site behaves identically. What changed is that your first visit got a
sticky_cid cookie, and every later request of yours goes back to the same
worker. Log in from two different browser profiles and you may well be served by two different
processes — the fastest way to feel what the pool does.
6. Watch what is happening
The counters, always available:
curl -s http://127.0.0.1:8098/metrics
The live monitor is served by genro-asgi at
/_server/monitor/, and every one of its routes is gated
SERVER_ADMIN. The built-in recipe declares no administrator, so out of the
box the monitor answers 401. To open it, launch with a config that declares
one — and, because the user store encrypts at rest, a storage key as well:
# monitor_config.py
import os
from genro_bag.resolvers import EnvResolver
from genro_storage import StorageManager
from genro_asgi.config import AsgiConfigBuilder
from genropy_asgi.spa import GenropySpaApplication
SITE = os.environ.get("GNR_ASGI_PATH") or "test_invoice_pg"
PORT = int(os.environ.get("GNR_ASGI_PORT") or 8098)
class ServerConfiguration(AsgiConfigBuilder):
def main(self, root):
cfg = root.configuration()
cfg.server(host="127.0.0.1", port=PORT)
cfg.middleware()
cfg.storage(app=StorageManager, storage_key=EnvResolver("GENRO_STORAGE_KEY"))
auth = cfg.authentication()
auth.admin_password(EnvResolver("GNR_ADMIN_PASSWORD"))
cfg.applications().application(
code="site",
app_class=GenropySpaApplication,
source=SITE,
debug=False,
workers=2,
local_worker=False,
)
# a Fernet key for the at-rest encryption of the user store
export GENRO_STORAGE_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
export GNR_ADMIN_PASSWORD=<choose one>
gnrasgiserve test_invoice_pg --config monitor_config.py -p 8098
Then open http://127.0.0.1:8098/_server/login_page in the browser, sign in
as admin with that password, and go to
/_server/monitor/. From a script it is a form post:
curl -s -c jar.txt -d 'identity=admin&password=<pwd>' \
http://127.0.0.1:8098/_server/login
curl -s -b jar.txt http://127.0.0.1:8098/_server/monitor/snapshot
Honest scope. The monitor renders the generic panel for the site
application today: you see the server, its sections and the mounted app, not a per-worker
breakdown. The placement view — which user sits on which worker — exists inside the
front but is not exposed over HTTP yet. For now, /metrics plus the launch
log is what you read from outside.
7. The load harness, if you want numbers
benchmarks/ holds a stdlib-only harness: a faithful replay of a recorded
browser session, driven at rising concurrency against a running instance. Nothing to install, and
its README.md carries the recipe that works, including the traps. Run every
script from inside that directory — they open their data files by relative name.