Skip to content

perf(app): index files.dataset_id and remove per-row work in lifecycle tasks - #1547

Draft
ariostas wants to merge 7 commits into
ssl-hep:developfrom
ariostas:perf/app-queries
Draft

ariostas wants to merge 7 commits into
ssl-hep:developfrom
ariostas:perf/app-queries

Conversation

@ariostas

@ariostas ariostas commented Sep 9, 2026

Copy link
Copy Markdown

🤖 AI text below 🤖

Cuts the per-row and per-request work out of the app's hot paths and bounds its outbound HTTP calls: the two lifecycle endpoints now do their deletions in bulk inside a single transaction, the files table gets back the index it needs, authenticated requests decode their JWT once instead of twice, the presigning S3 client is reused, and no outbound request can hang forever.

How to review. Each finding is one commit; the Commits tab shows them in severity order. Each entry below links to its commit; tick approve or reject under it. To ask for a change instead, leave a review comment on the commit. Rejected commits will be dropped from the branch and this list will be updated to match.

  • B32 · medium · ec37cf6 · Give REQUEST_TIMEOUT a real read timeout and add one to the VPS liveness request
    • approve
    • reject
  • B33 · medium · 2ea3d89 · Remember only images confirmed to exist, so failed registry lookups are retried
    • approve
    • reject
  • B55 · medium · 80ffe44 · Restore the index on files.dataset_id and declare the existing submit_time index on the model
    • approve
    • reject
  • P1 · perf · d872baa · Bulk update in dataset obsoletion, one transaction and eager loading in the data lifecycle task
    • approve
    • reject
  • P2 · perf · 15f6c33 · Empty buckets with minio's batched remove_objects and log through the app logger
    • approve
    • reject
  • P6 · perf · 0e271c1 · Build the presigning S3 client once per set of object store settings and compute the expiry in UTC
    • approve
    • reject
  • P7 · perf · eae77b5 · Stop verifying the JWT a second time inside get_jwt_user
    • approve
    • reject

Follow-ups noticed, out of scope

  • PR fix(app): guard request status transitions and return client errors as 4xx #1549 adds a migration with the same parent revision (v1_8_4); whichever of the two merges second needs its down_revision re-pointed at the other.
  • servicex_app_test/test_transformer_manager.py passes only as part of the full suite: test_init_invalid_config and test_persistent_claim_exists rely on an application context that an earlier test file leaves pushed. This is pre-existing and untouched here, but it makes the file impossible to run on its own.
  • urllib3.Timeout does not implement __eq__, so the VPS liveness test compares connect_timeout and read_timeout by hand instead of using assert_called_with. A small comparison helper would read better if more call sites start passing timeouts.
  • The sidecar has the other half of P6: transformer_sidecar/src/transformer_sidecar/transformer.py constructs a fresh ObjectStoreManager, and therefore a fresh minio client, in two places rather than reusing one.
  • Dataset.find_by_id in models.py still uses the legacy cls.query.get(id), which SQLAlchemy 2.0 deprecates in favour of db.session.get; the other two call sites in the same file were already converted.

Part of #1539.

The shared REQUEST_TIMEOUT was (0.5, None), so a hung code generator, Mailgun
or Slack endpoint could pin a worker forever waiting for a response, and the
VPS liveness request had no timeout at all. Both now use bounded connect and
read timeouts.

Refs ssl-hep#1539
Assisted-by: claude-code:claude-opus-5
check_image_exists memoized its whole result, so a registry timeout or an image
pushed a moment later stayed rejected for the life of the worker. Only images
confirmed to exist are remembered now, and failed lookups are retried.

Refs ssl-hep#1539
Assisted-by: claude-code:claude-opus-5
Migration 1.5.6 dropped the index on files.dataset_id and never restored it, so
dataset.files, TransformRequest.all_files and the per-dataset delete in the
lifecycle task scanned the whole table. This adds the index back and declares
the already existing index on TransformRequest.submit_time on the model so it
matches the schema.

Refs ssl-hep#1539
Assisted-by: claude-code:claude-opus-5
Dataset obsoletion loaded every dataset and filtered in Python with a commit per
row; it now issues a single UPDATE. The data lifecycle endpoint deletes expired
transforms and orphaned datasets inside one transaction instead of committing
per row and then touching objects the commit had expired, eager loads the
submitting user, and returns 400 for a missing or unparseable cutoff_timestamp.

Refs ssl-hep#1539
Assisted-by: claude-code:claude-opus-5
Emptying a bucket issued one remove_object call per key. It now uses minio's
batched remove_objects, consumes the returned error iterator, and logs through
the app logger instead of print.

Refs ssl-hep#1539
Assisted-by: claude-code:claude-opus-5
FileURLGenerator built a boto3 client on every request. The client is now built
once per set of object store settings, and the presigned URL expiry is computed
in UTC so it no longer depends on the server's local timezone.

Refs ssl-hep#1539
Assisted-by: claude-code:claude-opus-5
get_jwt_user was wrapped in a JWT check even though both callers, auth_required
and is_admin_user, already call verify_jwt_in_request first, so every
authenticated API request decoded and verified its token twice. The test fixture
that had been hiding this now mocks the identity lookup directly.

Refs ssl-hep#1539
Assisted-by: claude-code:claude-opus-5
todelete = [
_.id for _ in datasets if _.last_updated and (now - _.last_updated) > delta
]
cutoff = now - delta

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High priority change


expirydelta = 365 * 24 * 60 * 60
expiry = int(datetime.datetime.now().timestamp() + expirydelta)
expiry = int(datetime.now(timezone.utc).timestamp() + expirydelta)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High priority change

This branch has not been deployed

No deployments
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.

2 participants