Replace rest-client with a persistent HTTP client for worker->web calls#847
Merged
Conversation
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>
This was referenced Jul 16, 2026
Merged
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.
Problem
Every worker
RestClientcall inRunSimulateDataPointopens 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 exhaustsnf_conntrack_maxon 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:
Net::HTTP#set_formdefaults file parts toapplication/octet-stream;download_result_filestores the part type and serves onlytext/html/application/json/text/plaininline, so HTML/JSON reports would force-download. The client now mime-maps by extension exactly as rest-client'sMIME::Typesguess did (part opts must use Symbol keys - String keys are silently ignored).OsHttp::Error(aStandardError),.codeis an Integer,JSON.parse(response)works viato_str- existing retry loops behave unchanged.idle_timeout(15s) below Puma's 20s persistent timeout so idle connections are reopened instead of racing server-side closes on POSTs.os_server_host_urlchanges (the run_simulation feature specs repoint it at a per-process Capybara server; rest-client readAPP_CONFIGper call).rest-clientmoves 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
server/spec/lib/os_http_spec.rb, wired intorun-server-tests.sh) assert the actual claim: sequential requests share one accepted TCP connection, reconnect after server close, error/response parity, multipart wire format.dj_run_simulation_data_point_specandresque_run_simulation_data_point_specin 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:Completed the EnergyPlus simulation; assert onEnergyPlus returned '0'instead.Analysis#before_destroyenqueuesDjJobs::DeleteAnalysis(anrm_rfof 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 deferredrm_rfdeleted the shared analysis dir mid-run of the next spec. Newdestroy_projects_inlinehelper forces the deletion inline (also applied to the Enhance InitializeAnalysis resilience for corrupt seed.zip files #841 spec hooks that race them), plus amkdir_pguard so the initialize-worker error path cannot ENOENT and mask the real failure.eplustbl.html->text/html,objectives.json->application/json(inline disposition preserved).Note:
fix/run-simulate-data-point-hardeningtouches the same job file in different hunks; happy to coordinate merge order.🤖 Generated with Claude Code