SQLAlchemy v2: production fixes on top of #150 - #153
Open
djgalloway wants to merge 13 commits into
Open
djgalloway wants to merge 13 commits into
djgalloway wants to merge 13 commits into
Conversation
Also: - update pecan - update psycopg to v3 - drop statsd Signed-off-by: Zack Cerza <zack@cerza.org>
Signed-off-by: Zack Cerza <zack@cerza.org>
Signed-off-by: Zack Cerza <zack@cerza.org>
This endpoint's logic was a little strange and was also implemented as requiring a POST request. Simplify it and correct its behavior. Also fix a test bug. Signed-off-by: Zack Cerza <zack@cerza.org>
Before: * Used global thread-scoped sessions * Sessions could leak between requests causing incorrect updates * Tests were difficult to get right After: * Request-scoped sessions that are isolated from each other * Lifecycle managed in one place * Guaranteed cleanup on error * Tests are more terse and easily understood * Pytest fixtures which make tests composable and more maintainable * Tests do not persist any state to the database * Eaasy to invoke integration tests Signed-off-by: Zack Cerza <zack@cerza.org>
Signed-off-by: Zack Cerza <zack@cerza.org>
* Update python and actions versions * We don't use travis or nose Signed-off-by: Zack Cerza <zack@cerza.org>
Signed-off-by: Zack Cerza <zack@cerza.org>
This lets us simplify error handling in controllers, and to avoid ever returning HTML inadvertently Signed-off-by: Zack Cerza <zack@cerza.org>
Run the full test suite with a live paddles server backed by an ephemeral PostgreSQL service container so race and HTTP integration tests execute in CI. Signed-off-by: Zack Cerza <zack@cerza.org>
This fixes failures that I only saw in GitHub Actions Signed-off-by: Zack Cerza <zack@cerza.org>
Resolve conflicts with the Job.package_source change (#151): keep the SQLAlchemy 2 requirements, add package_source to the v2 Job model, port its tests to the new fixtures, and chain the sqla v2 migration after 3eb94eceb2cc so `alembic upgrade head` works against a database that already carries package_source. Assisted-by: Claude Fable 5.1 Signed-off-by: David Galloway <david.galloway@ibm.com>
Ran the port against a full copy of the production paddles database and fixed everything that broke teuthology, pulpito, deployment or the migration. All changes have regression tests (paddles/tests/controllers/test_teuthology_flows.py). Deployment / driver: - paddles.db.get_engine() now rewrites postgresql:// (and postgresql+psycopg2://) URLs to postgresql+psycopg:// so the existing prod secret and Crunchy's pgbouncer-uri keep working without editing every secret. alembic/env.py normalizes the same way and its PGPASS hook uses psycopg. Migration (b6c4013b876e): - Chained after 3eb94eceb2cc (Job.package_source) instead of cf54532418e7, so `alembic upgrade head` works on a database that already carries package_source. - Added a data-cleanup step before the NOT NULL / UNIQUE constraints. Production violates all of them: 3 nodes with up IS NULL, 40 legacy nodes with machine_type IS NULL, 78 job rows with job_id IS NULL (they carry no other data and are unreachable via the API), and 112 duplicate (run_id, job_id) pairs. Without this the migration aborts on the first statement and, under container_start.sh's `set -e`, every new pod crashloops. - Named the unique constraint (uq_jobs_run_id_job_id) so downgrade() can drop it; the previous downgrade could not even compile. Job / Run behavior: - Job.updated (and the parent Run.updated) advance on every write again, including a status-less PUT, which teuthology's supervisor watchdog relies on and which stale-lock detection and pulpito runtimes read. - job_nodes is populated on update as well as insert, so /nodes/<n>/jobs/, node job_stats, the reimage-failure detector and testnode-monitor keep working (teuthology sends targets by PUT after locking, not at POST). - Run.status is a stored, indexed column again, recomputed via Run.set_status()/refresh_status() whenever a job's status changes or a job is deleted. /runs/status/<status>/ went from ~25s back to sub-second and no longer full-scans with correlated subqueries. - success is stored alongside status (teuthology always sends both). - Job.update() only sets real columns and reconciles status/success; unknown or protected keys (id, posted, ...) no longer 500, and an invalid status is a 400 not a 500 (validator format string fixed). - POST accepts a job whose body carries "queue": null next to a job_id; the generated-id (paddles-as-queue) path ignores non-numeric ids. Controllers / hooks: - Trailing-slash redirects keep their Location header: the error hook lets 3xx through, and the session hook no longer rolls back on a redirect or other <400 response. teuthology's get_run()/get_jobs(run, id) depend on following these redirects. - /jobs/?posted_after/posted_before compare against parsed datetimes, not raw strings (500 under psycopg3's typed parameters). - Run filter-index endpoints (/runs/branch/, /runs/machine_type/, ...) dedupe and drop NULLs again, so a single NULL value no longer 500s the whole list. - CorsHook and SentryHook are wired back into the app. Timestamps: - Added paddles.util.utcnow() (naive UTC) and use it everywhere a timestamp is written, so stored values and JSON output don't depend on the server's TimeZone and don't drift the way tz-aware writes into `timestamp without time zone` columns would. Maintenance commands: - Ported every pecan command (expire_jobs, set_status, dedupe, delete, reparse, node_jobs, node_stats, queue_stats, set_targets) onto a small SessionCommand base that opens a real SQLAlchemy 2 session, replacing the removed models.start()/commit()/Query API. Dropped import_nodes, which targeted a lock server that no longer exists. Assisted-by: Claude Fable 5.1 Signed-off-by: David Galloway <david.galloway@ibm.com>
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #150 (SQLAlchemy 2 port by @zmc). This branch merges
maininto that work and fixes the problems that surface when it runs against a full copy of the production Sepia database. Opening it separately rather than pushing onto #150 so the port and the fixes can be reviewed independently; happy to fold it in if preferred.Every fix has a regression test in
paddles/tests/controllers/test_teuthology_flows.py.toxis green (201 passed, 5 skipped) andflake8is clean.How it was tested
Cloned the production
paddles-db(41 GB, 138k runs, 8.6M jobs, 1,156 nodes) into a throwaway namespace, built this branch's image in-cluster, applied the migration to the copy, and drove it with the exact HTTP flows teuthology (report.py,lock/ops.py,lock/query.py,dispatcher/supervisor.py) and pulpito use, comparing every read against production.Deployment / driver
paddles.db.get_engine()rewritespostgresql://(andpostgresql+psycopg2://) URLs topostgresql+psycopg://. Under SQLAlchemy 2 a barepostgresql://URL selects psycopg2, which this PR removes, so the deployed secret and Crunchy's generatedpgbouncer-uriwould otherwise fail at startup and in the DB-connectivity probes.alembic/env.pynormalizes the same way and itsPGPASShook uses psycopg 3.Migration
b6c4013b876e3eb94eceb2cc(Job.package_source, Add Job.package_source #151) instead ofcf54532418e7, soalembic upgrade headworks on a database that already carriespackage_source.NOT NULL/UNIQUEconstraints. Production data violates all of them: 3 nodes withup IS NULL, 40 legacy nodes withmachine_type IS NULL, 78 job rows withjob_id IS NULL(they carry no other data and are unreachable through the API), and 112 duplicate(run_id, job_id)pairs. Without the cleanup the migration aborts on its first statement and, undercontainer_start.sh'sset -e, every new pod crashloops.uq_jobs_run_id_job_idsodowngrade()can drop it; the previousdowngrade()could not compile (DROP CONSTRAINTon an unnamed constraint).ALTER TABLEs holdACCESS EXCLUSIVEonjobsfor a full-table scan (~13 s for 8.5M rows). The migration docstring says to deploy it deliberately, not through an unattended ImageStream rollout.Job / Run behavior
Job.updatedand the parentRun.updatedadvance on every write again, including a status-less PUT — which the supervisor watchdog sends purely to bumpupdated, and which stale-lock detection and pulpito runtimes read.job_nodesis populated on update as well as insert. teuthology sendstargetsby PUT after locking, not at POST, so linking only on insert left/nodes/<name>/jobs/, nodejob_stats, the reimage-failure detector and testnode-monitor with no new data.Run.statusis a stored, indexed column again, recomputed viaRun.set_status()/refresh_status()when a job's status changes or a job is deleted./runs/status/<status>/(pulpito's running-runs filter) went from ~25 s of correlated-subquery full scans back to sub-second.successis stored alongsidestatus(teuthology'sset_status()always sends both).Job.update()sets only real columns and reconcilesstatus/success; unknown or protected keys (id,posted, ...) no longer 500, and an invalid status is a 400, not a 500 (the validator's%-format on a tuple was raisingTypeError)."queue": nullnext to ajob_id, and the generated-id path ignores non-numeric ids.Controllers / hooks
Locationheader: the error hook lets 3xx through, and the session hook no longer rolls back on a redirect or other <400 response. teuthology'sget_run()/get_jobs(run, id)build slash-less URLs and rely on following the redirect; previously they got aLocation-less 302 and a{"message": "Found"}body./jobs/?posted_after=&posted_before=compares against parsed datetimes, not raw strings (500 under psycopg 3's typed parameters)./runs/branch/,/runs/machine_type/, ...) dedupe and drop NULLs again; a single NULL value was 500ing the whole list.CorsHookandSentryHookare wired back into the app.Timestamps
paddles.util.utcnow()(naive UTC) and use it wherever a timestamp is written, so stored values and JSON output don't depend on the server'sTimeZoneand don't drift the way tz-aware writes intotimestamp without time zonecolumns can.Maintenance commands
pecancommand (expire_jobs,set_status,dedupe,delete,reparse,node_jobs,node_stats,queue_stats,set_targets) onto a smallSessionCommandbase that opens a real SQLAlchemy 2 session, replacing the removedmodels.start()/commit()/QueryAPI that made them fail to import. Droppedimport_nodes, which targeted a lock server that no longer exists.🤖 Prepared with Claude Fable 5.1.