Skip to content

Replace rest-client with a persistent HTTP client for worker->web calls#847

Merged
brianlball merged 5 commits into
developfrom
conntrack-persistent-http
Jul 16, 2026
Merged

Replace rest-client with a persistent HTTP client for worker->web calls#847
brianlball merged 5 commits into
developfrom
conntrack-persistent-http

Conversation

@brianlball

Copy link
Copy Markdown
Contributor

Problem

Every worker RestClient call in RunSimulateDataPoint opens a fresh TCP connection and leaves a TIME_WAIT socket behind; each closed connection also holds an nf_conntrack entry for ~120s. A datapoint job makes ~8-20 such calls (delete result_files, datapoint/analysis JSON gets, 5-15 report uploads). At high worker counts on the Kubernetes deployment path this churn exhausts nf_conntrack_max on the nodes hosting the web tier.

Fix

Route the five call sites through a per-process persistent client (OsHttp, backed by net-http-persistent, already in the bundle): ~8-20 connections per job drop to ~1-2. Resque forks a child per job and the client is built lazily, so each child opens its own connection on first use; DJ workers are long-lived and reuse the connection across jobs.

Based on a patch by Dan, with the following behavior-parity fixes:

  • Multipart part Content-Type: Net::HTTP#set_form defaults file parts to application/octet-stream; download_result_file stores the part type and serves only text/html/application/json/text/plain inline, so HTML/JSON reports would force-download. The client now mime-maps by extension exactly as rest-client's MIME::Types guess did (part opts must use Symbol keys - String keys are silently ignored).
  • Non-2xx raises OsHttp::Error (a StandardError), .code is an Integer, JSON.parse(response) works via to_str - existing retry loops behave unchanged.
  • Client idle_timeout (15s) below Puma's 20s persistent timeout so idle connections are reopened instead of racing server-side closes on POSTs.
  • Singleton rebuilds if os_server_host_url changes (the run_simulation feature specs repoint it at a per-process Capybara server; rest-client read APP_CONFIG per call).
  • rest-client moves to the test group: the live-stack specs still drive the API with it; specs that relied on Bundler auto-require now require it explicitly.

Testing

  • New socket-level specs (server/spec/lib/os_http_spec.rb, wired into run-server-tests.sh) assert the actual claim: sequential requests share one accepted TCP connection, reconnect after server close, error/response parity, multipart wire format.
  • Re-enabled dj_run_simulation_data_point_spec and resque_run_simulation_data_point_spec in the docker CI job - they are the job-level integration tests for this path but had not run in CI since 2020. Verified green (7/7 and 2/2, two clean rounds) against the full docker-compose test stack, which required two bitrot/infra fixes:
    • openstudio-workflow no longer logs Completed the EnergyPlus simulation; assert on EnergyPlus returned '0' instead.
    • Fixed a cross-spec deletion race: Analysis#before_destroy enqueues DjJobs::DeleteAnalysis (an rm_rf of the analysis dir). Cleanup hooks outside the foreground around-wrapper enqueued it for the web-background container to run later, and because the spec formulations reuse a fixed analysis uuid, the deferred rm_rf deleted the shared analysis dir mid-run of the next spec. New destroy_projects_inline helper forces the deletion inline (also applied to the Enhance InitializeAnalysis resilience for corrupt seed.zip files #841 spec hooks that race them), plus a mkdir_p guard so the initialize-worker error path cannot ENOENT and mask the real failure.
  • Stored result-file content types verified live from the stack run: eplustbl.html -> text/html, objectives.json -> application/json (inline disposition preserved).

Note: fix/run-simulate-data-point-hardening touches the same job file in different hunks; happy to coordinate merge order.

🤖 Generated with Claude Code

brianlball and others added 5 commits July 16, 2026 09:42
Every worker RestClient call opened a fresh TCP connection and left a
TIME_WAIT socket whose nf_conntrack entry lingers ~120s. At high worker
counts this exhausts nf_conntrack_max on nodes hosting the web tier.
Route the five RunSimulateDataPoint call sites through a per-process
net-http-persistent client (already in the bundle): ~8-20 connections
per datapoint job drop to ~1-2.

Parity details:
- Response duck-types RestClient::Response (Integer #code, JSON.parse
  via #to_str); non-2xx raises OsHttp::Error (StandardError) so the
  existing retry loops behave the same
- Multipart file parts keep rest-client's basename filename and
  extension-based Content-Type. Load-bearing: download_result_file
  stores the part Content-Type and only serves text/html,
  application/json, text/plain inline
- Client idle_timeout (15s) below Puma's 20s persistent timeout so we
  reopen idle connections instead of racing server-side closes
- rest-client moves to the test group: docker_stack/live-stack specs
  still drive the API with it; specs that relied on Bundler
  auto-require now require it explicitly

Socket-level specs (spec/lib/os_http_spec.rb) assert the actual claim:
sequential requests share one accepted TCP connection, reconnect after
server close, error/response parity, and multipart wire format. Wired
into run-server-tests.sh; docker_stack_algo_spec exercises the client
end-to-end via real analyses.

Based on a patch by Dan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The run_simulation feature specs repoint APP_CONFIG['os_server_host_url']
at a per-process Capybara server after boot. rest-client read APP_CONFIG
on every call so this Just Worked; a memoized client must notice the URL
change and rebuild, or those specs (and any runtime reconfig) would talk
to the wrong host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These are the job-level integration tests for the worker->web HTTP path,
but they had not run in CI since 2020 and had bitrotted. Verified green
against the full docker-compose test stack (2 clean rounds).

- Fix stale assertion: openstudio-workflow no longer logs 'Completed the
  EnergyPlus simulation'; assert on "EnergyPlus returned '0'" instead
- Fix a cross-spec deletion race: Analysis#before_destroy enqueues
  DjJobs::DeleteAnalysis (rm_rf of the analysis dir). Cleanup hooks
  outside the foreground around-wrapper enqueued it for the
  web-background container to run later, and because the spec
  formulations reuse a fixed analysis uuid the deferred rm_rf deleted
  the shared analysis dir mid-run of whichever spec came next
  (initialize_worker then failed after ~60s of retries). New
  destroy_projects_inline helper forces the deletion inline; used in
  the run_simulation hooks and the #841 spec hooks that race them
- initialize-worker failure path: mkdir_p simulation_dir before writing
  the error out.osw so the report cannot ENOENT and mask the real error
- after(:all) project cleanup + rm -rf of the root-owned
  assets/data_points dir in run-server-tests.sh so the docker_stack
  specs that follow start empty and writable (#841 lesson)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PAT-local CI env (openstudio_meta run_rspec) loads delayed_job but
never defines Resque, so the unguarded Resque.inline reference raised
NameError from the before/after(:all) hooks and failed every example in
the groups using the helper. Windows dev setups have the mirror problem
(both queue gems are linux-only in the Gemfile). Reference each constant
only if defined.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
on: [push, pull_request] ran every workflow twice for branches with an
open PR (~2x docker-stack builds per push). Trigger pull_request for
feature branches, push only for the branches deploy_docker_github_actions
keys on (develop/master/x.y.z/*-LTS). Concurrency group cancels
superseded PR runs on rapid re-pushes; push runs never cancel so
develop/master image uploads cannot be interrupted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@brianlball
brianlball merged commit 254f82b into develop Jul 16, 2026
5 checks passed
@brianlball
brianlball deleted the conntrack-persistent-http branch July 16, 2026 17:46
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.

1 participant