Fail fast on corrupt seed.zip: validate at upload, no futile retries, terminal 'failed' state (#841) - #844
Conversation
…lysis failed - Analysis.seed_zip_error: read-only zip validation (structure + per-entry CRC); used by AnalysesController#upload/#create to reject corrupt seed zips with 422 - Analysis#run_initialization: Zip/Zlib errors are deterministic - fail on first attempt (no 3x retry) and remove partial extraction; transient errors still retry - ResqueJobs::InitializeAnalysis.perform: on failure set job status 'failed' + status_message so the analysis reaches a terminal API-visible state, then re-raise so Resque records the failed job - specs: analysis_init_spec.rb (model/job), analyses_upload_spec.rb (422 contract) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The linux/macos CI jobs already run the full server spec suite via 'openstudio_meta run_rspec', which reproduces the PAT local deployment (delayed_job, local mongo) and writes rspec output to spec/unit-test/logs/rspec.log rather than the CI console. The docker job has not run server model/request specs since Sept 2020 (4958092) - it runs only the docker_stack feature specs. Issue #841 is specific to the resque code path (InitializeAnalysis only runs under resque), so also run the new #841 spec files in the docker job, where RAILS_ENV=docker, resque/redis, and authenticated mongo are in play. They need only rails+mongo, finish in about a second, and leave the database empty, so they run before the feature specs. Verified in a local replica of the CI docker environment (nrel/openstudio-server:develop image + mongo as 'db' + redis as 'queue'): 12 examples, 0 failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rking The docker job runs rspec as root inside the web container while the live app serves as an unprivileged user (nobody). The new #841 specs wrote seed zips through paperclip into /mnt/openstudio/server/assets/analyses, leaving that directory root-owned (755). Every later upload from the live app then failed with Errno::EACCES -> HTTP 500, which broke the docker_stack feature specs (custom_gems, 9/10 algo). Leftover factory projects also broke the empty-projects assertion in docker_stack_test_apis_spec. Fix: after(:all) Project.destroy_all in both spec files - paperclip deletes the attachments and prunes the emptied root-owned directory, so the live app recreates it under its own user. Verified in a fresh local replica of the CI docker environment, in CI order (specs first as root, then app-user traffic): 12 examples 0 failures, assets/analyses pruned, projects list empty, valid upload 201 (dir recreated by nobody), corrupt upload 422 through the live nginx/Passenger stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The first docker-job run of the new CI line failed — root-caused and fixed in 3721a72: The 12 new specs themselves passed in the docker (resque) environment, but they broke the feature specs that run after them. Fix: 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Pull request overview
This PR addresses incident #841 by preventing corrupt seed.zip files from entering the analysis pipeline, avoiding futile initialization retries on deterministic ZIP/Zlib failures, and ensuring analyses reach an API-visible terminal failed state when initialization fails (while still letting Resque record the job as failed).
Changes:
- Add deep ZIP validation (
Analysis.seed_zip_error) and return422 Unprocessable Entityduringcreate/uploadwhen a corrupt/truncated/invalid ZIP is detected. - Fail fast on
Zip::Error/Zlib::Errorduring initialization (no retry) and clean up partial extraction directories. - Add Resque job-level rescue handling to mark the analysis/job as
failedbefore re-raising, plus add targeted specs and run them in the docker server test script.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| server/spec/requests/analyses_upload_spec.rb | Adds request coverage for 422 rejection of corrupt/truncated uploads and 201 acceptance of valid ZIPs. |
| server/spec/models/analysis_init_spec.rb | Adds model/job specs for ZIP validation, fail-fast extraction behavior, cleanup, and terminal failure state propagation. |
| server/app/models/analysis.rb | Introduces seed_zip_error, fail_job!, and fail-fast ZIP/Zlib handling in run_initialization. |
| server/app/jobs/resque_jobs/initialize_analysis.rb | Marks analysis/job failed on initialization exceptions, then re-raises so Resque records failure. |
| server/app/controllers/analyses_controller.rb | Validates uploaded/created seed ZIPs and returns 422 with a descriptive error_message on corruption. |
| docker/server/run-server-tests.sh | Ensures the new model/request specs run in the docker (Resque/Redis/Mongo) CI path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Reject corrupt seed zips at creation time with a 422 instead of letting them fail | ||
| # later in ResqueJobs::InitializeAnalysis and strand the analysis (issue #841) | ||
| if params[:seed_zip].respond_to?(:path) | ||
| zip_error = Analysis.seed_zip_error(params[:seed_zip].path) | ||
| if zip_error | ||
| respond_to do |format| | ||
| format.html { render action: 'new', status: :unprocessable_entity } | ||
| format.json { render json: { status: 'error', error_message: zip_error }, status: :unprocessable_entity } | ||
| end | ||
| return | ||
| end | ||
| end | ||
|
|
There was a problem hiding this comment.
Good catch that this path was broken, though the failure mode is different: there is no new.html.erb view in the app at all (the HTML create path is dead code — analyses are created via the JSON API), so render action: 'new' raises ActionView::MissingTemplate regardless of @analysis. The pre-existing error branch below has the same landmine. Fixed in a8c8855 by rendering a plain-text 422 instead of a template.
| def self.seed_zip_error(file_path) | ||
| # Zip::File.new instead of the Zip::File.open block form: open's implicit close | ||
| # calls commit, which can REWRITE the archive being validated. new reads the | ||
| # central directory and releases the file handle, keeping validation read-only. | ||
| zf = ::Zip::File.new(file_path) | ||
| zf.each do |entry| | ||
| next unless entry.file? | ||
|
|
||
| crc = ::Zlib.crc32 | ||
| entry.get_input_stream do |io| | ||
| while (chunk = io.read(1_048_576)) | ||
| crc = ::Zlib.crc32(chunk, crc) | ||
| end | ||
| end | ||
| return "seed zip entry '#{entry.name}' is corrupt (CRC mismatch)" unless crc == entry.crc | ||
| end |
There was a problem hiding this comment.
Agreed — addressed in a8c8855 with streaming caps counted inside the read loop: SEED_ZIP_MAX_INFLATED_BYTES (10 GB) and SEED_ZIP_MAX_ENTRIES (100k), each returning a descriptive 422-able error when exceeded. No Timeout needed since we control the loop, and no async validation — deferring the check would reintroduce exactly the delayed-failure problem #841 is about. Covered by two new specs using stub_const with tiny caps. Worth noting the pre-existing extraction path in run_initialization still inflates unbounded zips (with a 1-hour timeout) on the web node; bounding that is a separate concern from this PR.
- Analysis.seed_zip_error: cap validation work at SEED_ZIP_MAX_INFLATED_BYTES (10 GB) and SEED_ZIP_MAX_ENTRIES (100k), counted while streaming, so a zip bomb cannot pin a web worker in the request path; 2 new specs via stub_const - AnalysesController#create corrupt-zip path: render plain-text 422 for HTML - 'render action: new' would raise MissingTemplate (no new.html.erb exists) - InitializeAnalysis: bare 'raise' instead of 'raise e' (style only - both preserve the original backtrace when re-raising the same exception object) Verified: 11 model examples locally, 14 total in the CI docker-env replica. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d stale-job re-dispatch Root cause of 2026-07-07/08 production incident: 50+ analyses with corrupted seed zips caused `write_lock` (dj_jobs/run_simulate_data_point.rb) to release its flock on failure but never delete the lock file, and never write a receipt. The waiter loop only polled for the receipt file and never rechecked the lock, so every subsequent worker pod for the same analysis blocked forever on `initialize_worker`, waiting out the full 8h `initialize_worker_timeout` and then (due to a second bug) falling through to `return true` anyway -- reporting fake success and letting `perform` proceed to simulate against a directory that was never populated. Fleet-wide this produced ~9,988 permanently-zombied worker pods (99.6% of the tier). Fixes: - `write_lock`: rescue any exception in the protected block, delete the lock file before re-raising (previously only released the flock, leaving a file a future worker could stat as "still locked"). Also closes the file handle in `ensure` (minor fd leak). - `initialize_worker`'s receipt-wait loop: detect `lock_holder_gone` (lock file disappears without a receipt appearing -- the crash/failure signature) and return `false` instead of looping to timeout. On genuine `Timeout::Error`, explicitly delete the stale lock file and return `false` instead of falling through to `return true`. - Add a `rescue ::Zip::Error, ::Zlib::Error` branch ahead of the generic retry rescue in the zip-extraction path: fails fast on deterministic corruption (no futile 3x retry), removes the partial `analysis_dir`. Worker-side counterpart to the web-node fail-fast added for seed.zip validation in #841/#844 (a different, once-per-analysis code path; this is the per-datapoint, per-worker-pod download+extract path). - Fix `@@data_point` -> `@data_point` (uninitialized class variable vs the actual instance variable) in the invalid-timeout fallback branches: 3 occurrences in `dj_jobs/run_simulate_data_point.rb`, 2 more in `dj_jobs/urban_opt.rb` (included into the same class). Bug present since ebc2361 (2019), also confirmed on origin/develop. Previously any invalid timeout value raised NameError instead of applying the 28800s default. - `resque_jobs/run_simulate_data_point.rb#perform`: add a narrow `analysis.run_flag == false` guard (checked ahead of the existing completed-status guard) to skip stale queue payloads for analyses that were explicitly stopped/quarantined via `stop_analysis`/ `soft_stop_analysis`. Deliberately not a blanket `status == 'completed'` check, which would break the admin manual-retry workflow (`requeue`/ `requeue_started` do not reset status before re-enqueueing). Adds `server/spec/models/dj_run_simulate_data_point_hardening_spec.rb` (7 examples) and `server/spec/models/resque_run_simulate_data_point_hardening_spec.rb` (7 examples), all executed against a real local Mongo/Redis and passing. Each also has a companion "would fail against the pre-fix code" check to confirm discriminating power.
…d stale-job re-dispatch (#845) * Harden RunSimulateDataPoint worker against permanent lock deadlock and stale-job re-dispatch Root cause of 2026-07-07/08 production incident: 50+ analyses with corrupted seed zips caused `write_lock` (dj_jobs/run_simulate_data_point.rb) to release its flock on failure but never delete the lock file, and never write a receipt. The waiter loop only polled for the receipt file and never rechecked the lock, so every subsequent worker pod for the same analysis blocked forever on `initialize_worker`, waiting out the full 8h `initialize_worker_timeout` and then (due to a second bug) falling through to `return true` anyway -- reporting fake success and letting `perform` proceed to simulate against a directory that was never populated. Fleet-wide this produced ~9,988 permanently-zombied worker pods (99.6% of the tier). Fixes: - `write_lock`: rescue any exception in the protected block, delete the lock file before re-raising (previously only released the flock, leaving a file a future worker could stat as "still locked"). Also closes the file handle in `ensure` (minor fd leak). - `initialize_worker`'s receipt-wait loop: detect `lock_holder_gone` (lock file disappears without a receipt appearing -- the crash/failure signature) and return `false` instead of looping to timeout. On genuine `Timeout::Error`, explicitly delete the stale lock file and return `false` instead of falling through to `return true`. - Add a `rescue ::Zip::Error, ::Zlib::Error` branch ahead of the generic retry rescue in the zip-extraction path: fails fast on deterministic corruption (no futile 3x retry), removes the partial `analysis_dir`. Worker-side counterpart to the web-node fail-fast added for seed.zip validation in #841/#844 (a different, once-per-analysis code path; this is the per-datapoint, per-worker-pod download+extract path). - Fix `@@data_point` -> `@data_point` (uninitialized class variable vs the actual instance variable) in the invalid-timeout fallback branches: 3 occurrences in `dj_jobs/run_simulate_data_point.rb`, 2 more in `dj_jobs/urban_opt.rb` (included into the same class). Bug present since ebc2361 (2019), also confirmed on origin/develop. Previously any invalid timeout value raised NameError instead of applying the 28800s default. - `resque_jobs/run_simulate_data_point.rb#perform`: add a narrow `analysis.run_flag == false` guard (checked ahead of the existing completed-status guard) to skip stale queue payloads for analyses that were explicitly stopped/quarantined via `stop_analysis`/ `soft_stop_analysis`. Deliberately not a blanket `status == 'completed'` check, which would break the admin manual-retry workflow (`requeue`/ `requeue_started` do not reset status before re-enqueueing). Adds `server/spec/models/dj_run_simulate_data_point_hardening_spec.rb` (7 examples) and `server/spec/models/resque_run_simulate_data_point_hardening_spec.rb` (7 examples), all executed against a real local Mongo/Redis and passing. Each also has a companion "would fail against the pre-fix code" check to confirm discriminating power. * Close the lock fd before deleting it: Windows cannot unlink open files The deadlock fix deleted files whose handles were still open, which works on Linux but is a silent no-op on Windows (FileUtils.rm_f/rm_rf swallow the EACCES) - and desktop/PAT-on-Windows is the deployment write_lock's own comment says it exists for. Found by running the new hardening specs on Windows: 12/14, with the write_lock-raise and corrupt-zip-cleanup examples failing exactly this way. - write_lock rescue: unlock + close the fd, then rm_f; ensure only closes if the rescue has not already done so - corrupt-zip rescue (runs inside write_lock, lock fd open): sweep the extracted content but leave the lock file for write_lock to delete; raise typed CorruptAnalysisZip (same message) and let initialize_worker's new rescue rmdir the emptied analysis_dir after the lock is gone. Dir.rmdir, not rm_rf: if another worker already re-locked and started re-populating the dir, rmdir fails harmlessly instead of deleting its in-progress files All 14 hardening specs pass on Windows (were 12/14) against a local mongo; message and behavior assertions unchanged for Linux. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix run_flag guard skipping never-started analyses; probe flock before clearing locks Two fixes surfaced by the merge-ref CI run (develop now includes the re-enabled run_simulation feature specs) and the Copilot review: 1. run_flag defaults to false and only flips true when an analysis is started, so guarding on run_flag alone skipped every datapoint submitted directly against a fresh analysis (batch upload + submit_simulation - the resque feature spec's flow, and a real API path). Require a start_time (jobs exist iff the analysis was started) before treating run_flag=false as "stopped". Hardening spec's stopped context now creates the Job a genuinely stopped analysis would have, plus a regression example for the never-started case. 2. The timeout path deleted the lock file unconditionally, but a holder that is alive-but-slow can legitimately outlive a single waiter timeout (each of its steps gets its own initialize_worker_timeout); deleting a live holder's lock lets a third worker initialize the same analysis_dir concurrently. Probe the flock (LOCK_EX|LOCK_NB) and only clear locks nobody holds. The same probe in the wait loop (two consecutive positives, a poll apart) now detects SIGKILL/OOM-killed holders immediately - the incident's failure mode, which the rescue-based cleanup can never see - instead of after the full 8h. Verified: hardening specs 15/15 on Windows + Linux-container; resque feature spec 2/2 and dj 7/7 against the full docker-compose stack on the rebased branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: brianlball <brian.ball@nrel.gov> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Update permissions for rspec_command and enhance README with MVP suitability details * Enhance external batch execution support: - Update developer guide to reflect UrbanOpt and custom gemfile support. - Modify README to clarify MVP suitability and resolved limitations. - Improve run_chunk.rb to support Windows scripts and enhance logging. - Update LHS sampling to generate histogram images using pure Ruby. - Refactor external_batch.rb to handle nil ENV variables gracefully. - Adjust ingester and packager to support UrbanOpt and gemfile analyses. - Revise tests to validate packaging for UrbanOpt and gemfile analyses. * Add Nomad executor implementation for external batch feature * Add Nomad job submission script for external batch executor * Add Nomad task wrapper script for external batch processing Create task_wrapper.sh that serves as the entrypoint for Nomad tasks in the external batch executor. The script: - Handles package retrieval from shared filesystem or S3 - Sets up execution environment (unsets Bundler vars, configures PATH) - Executes run_chunk.rb for assigned chunk index - Handles result storage back to shared filesystem or S3 - Implements proper error handling and logging - Supports configurable storage types via environment variables - Matches the interface expected by Nomad job templates * Add Nomad job template for batch processing * docs: add Nomad executor section to external_batch/README.md * Add Nomad executor unit tests for submit_nomad.rb and sync_results.rb. Includes tests for packaging, job submission, parameter handling, and result synchronization. Also adds a full pipeline test for Nomad executor. Ensures existing functionality remains unaffected. * external_batch/nomad: .env-based config, Ansible provisioning, task_wrapper fixes, doc cleanup BREAKING: docker-compose.yml no longer has hardcoded IP fallbacks for OS_SERVER_NOMAD_SSH_HOST or NOMAD_ADDR. Users must set these in .env. Changes: - docker-compose.yml: Remove hardcoded 10.60.126.125 from OS_SERVER_NOMAD_SSH_HOST; add NOMAD_ADDR env var to web and web-background services - .gitignore: Add .env (now un-tracked via git rm --cached) - .env.example: New template with placeholder values and instructions - Dockerfile: Add rsync package (required for SSH rsync bridge) - task_wrapper.sh: Use NOMAD_ALLOC_INDEX as fallback for --chunk-index; switch work dir to /tmp/nomad/task; use /usr/local/bin/run_chunk.rb; downgrade chunk failure to WARNING (non-fatal for individual data points) - job_array.hcl: raw_exec driver, nomad-client constraint, resources 2000MHz/4096MB/1000MB with tuning comments - ansible/: Complete provisioning playbook set (site.yml, 3 playbooks, 5 roles, create_cluster.py provisioner) - submit_nomad.rb / sync_results.rb: Update --ssh-host help text to use <NOMAD_SERVER_FLOATING_IP> placeholder - nomad/README.md: Update --ssh-host description, reference OS_SERVER_NOMAD_SSH_HOST - docs/external_batch_nomad_plan.md: Replace all hardcoded IPs with placeholders * ansible/README.md: document additive mode, --max, CLI flags, inventory merge - Quick Start: make auto-inventory path primary, manual copy secondary - Add additive mode section (auto-detect, skip server, offset indices, merge) - Add CLI flags reference table - Update 'what the script generates' to describe merge vs overwrite - Document --max, --no-auto-inventory, --inventory-output, --prefix flags * external batch Nomad/Ansible fixes: create_cluster refactor, task wrapper, job templates, .gitignore Ported from mvp-external-batch-fixes commit 3cc9d73 minus the parts that conflict with or duplicate develop: - dropped OpenStudio 3.10.0 / mongo 6.0.7 downgrades (develop stays 3.11.0 / mongo 8) - dropped bin/openstudio_meta bundler config-set rewrite (broke CI: persistent with/without group conflict between test and export install_gems runs) - dropped DJ/Resque job edits (hand-backports of fixes develop already has via #844/#845) - dropped empty ansible/tail file and personal .gitignore entries * Fix CI: rewrite nomad spec to script's real contract, ImageMagick-optional histograms - external_batch_nomad_spec: was stubbing a nomad CLI via --nomad-cmd, but submit_nomad.rb drives the Nomad HTTP API (/v1/jobs/parse -> /v1/jobs) and lays out <loc>/analysis_<id>/{package,results}; spec also died in before hook on an interpolating heredoc (NameError: output_flag), masking the mismatch. Rewritten against the HTTP API with an in-spec stub server. - nomad/sync_results.rb: NFS branch mirrors via FileUtils instead of rsync (absent on Windows dev boxes and slim containers; loop hung without it) - sampling/lhs.rb: preflight histogram attach non-fatal; paperclip needs ImageMagick identify, absent on ubuntu-24.04/macos-15 runners + PAT hosts (CRAN snapshot pin restore from the original commit is a no-op here: this branch is based on develop, which already has it via ba21522 / #849.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep tracked .env dev defaults from develop 71c2c39 moved compose config to .env.example + gitignored .env, but develop ships a tracked .env with dev-default credentials that docker-compose.yml requires out of the box (REDIS_PASSWORD etc). Keep both: .env.example documents the knobs, .env keeps a fresh clone bootable. Removing the tracked .env can be its own PR if wanted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * compose: drop runtime apt-get rsync hack; rsync is in the image now 71c2c39 installed rsync at container start because the published image lacked it; the same commit added rsync to the Dockerfile, and compose builds locally, so the runtime install is redundant. chmod of the optional ssh key moves to the operator (see external_batch/nomad/README). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Copilot review on #856 - sampling/lhs.rb: emit valid IHDR chunk (was IHLR + 12-byte pack, every histogram PNG invalid); require fileutils/tempfile explicitly - run_chunk.rb: bundle install once per runner process with completion marker keyed on Gemfile digest (was once per datapoint, racy on shared dirs); spawn .bat/.cmd via cmd.exe /c and .ps1 argv-style; stop rewriting CRLF->LF in Windows scripts - task_wrapper.sh: per-allocation work dir (NOMAD_TASK_DIR or mktemp, was hard-coded /tmp/nomad/task); dotfile/empty-dir-safe cp -a copies - submit_nomad.rb: validate --job-template/--package-location up front - external_batch.rb: require tmpdir; guard nil/empty sim_root_path - compose: mount tracked external_batch/nomad/ssh dir instead of gitignored key files (fresh-clone docker compose up worked never) - ansible: OpenStudio 3.10.0 -> 3.11.0 in examples/defaults per PR intent Verified: sampling_lhs + external_batch + external_batch_nomad specs (27 examples, 0 failures, 1 pending); generated PNG decodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ansible examples: OpenStudio 3.10.0 to match base branch 179 PR retargeted develop -> 179 (OS 3.10). Flip back to 3.11.0 when this work is promoted to develop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Alex Chapin <achapin@nrel.gov> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes #841.
Problem
During the July 2026 production incident, corrupt
seed.zipuploads produced thousands of failedResqueJobs::InitializeAnalysisjobs, all with"zlib error while inflating". A corrupt zip was accepted at upload, extraction was retried 3 times (a corrupt file never becomes valid on retry), and when the job finally failed the analysis was left inqueued/startedforever with no API-visible failure state and no cleanup —RunAnalysisis only enqueued from anafter_performhook, which Resque skips whenperformraises.One correction to the issue text: a failed Resque job does not block the queue (failed jobs go to the failed list and the worker moves on), and there is no Resque-level retry configured — the "3 times" in the error message is an inline retry loop in
Analysis#run_initialization. So one corrupt upload produces one failed job, and 2641 failed jobs implies roughly that many submissions (likely a client resubmitting). The incident data is worth checking for whether the failed queue held one analysis id or many. The fix targets the real damage: analyses stranded in a non-terminal state, futile retries, and no rejection at upload time.Changes
Reject corrupt zips at upload (422). New
Analysis.seed_zip_error(path)validates zip structure and inflates every entry to verify CRCs.AnalysesController#uploadand#createreturn422with a descriptiveerror_messageinstead of accepting a corrupt file. Validation is strictly read-only: it deliberately avoids theZip::File.openblock form, whose implicitclosecan commit and rewrite the archive being read.Fail fast on corrupt zips during initialization.
Analysis#run_initializationnow rescuesZip::Error/Zlib::Errorseparately: no retries (the failure is deterministic), the partially extracted directory is removed so a later re-run cannot skip-and-reuse stale files, and the error names the analysis. Transient errors keep the existing 3-attempt retry.Terminal, API-visible failure state.
InitializeAnalysis.performnow rescues, calls the newAnalysis#fail_job!(sets the job status tofailedplus a status message), and re-raises so Resque still records the failed job. Analyses no longer sit inqueuedforever;/analyses/:id/status.jsonreportsfailed.failedis a new job-status string;Analysis.status_statesis not referenced by any live code path.Client impact (PAT / OSAF / openstudio-analysis gem)
openstudio_meta run_analysis→OpenStudio::Analysis::ServerApi#new_analysis, which raises on any non-201 upload response, so the meta CLI exits nonzero and PAT reports the failure at submission time, with the server'serror_messagein PAT's console log. Scripted OSAF use of the gem gets the same immediate raise.failedstatus: PAT's run screen stops polling only oncompletedand otherwise displays the raw status string. Afailedanalysis therefore displays as "failed" while polling continues until the user stops it — the same behavior as today's eternally-"queued" stuck analysis, but now the user can see it failed and why (status_message). No string comparison in PAT breaks on the new value. A reasonable PAT follow-up is to also stop polling onfailed.openstudio_meta run_analysisis fire-and-forget, and the gem's remaining status checks are datapoint-level (dp[:status] == 'completed'for report downloads), which this change does not touch.Testing
server/spec/models/analysis_init_spec.rb(9 examples: validator accept/reject including a CRC-mismatch archive that opens and extracts cleanly, fail-fast with no retry plus partial-extraction cleanup, transient-error retry preserved,fail_job!, andInitializeAnalysis.performfailure handling) andserver/spec/requests/analyses_upload_spec.rb(3 examples: 422 for garbage and truncated uploads with descriptive errors, 201 for a valid zip). Red-green verified: with the app changes reverted, 10 of 12 fail (the transient-retry example intentionally passes on the old code).nrel/openstudio-server:developimage, mongo asdb, redis asqueue,RAILS_ENV=docker), and the neighboring existing specs (analysis_spec, requestanalyses_spec) still pass.openstudio_meta run_rspec, which reproduces the PAT local deployment (delayed_job, local mongo) and writes rspec output tospec/unit-test/logs/rspec.lograther than the console — easy to miss when reading CI logs. Both jobs were green on this branch with the new specs included.49580920) — and Enhance InitializeAnalysis resilience for corrupt seed.zip files #841 is specific to the resque path, which only the docker environment exercises. This PR adds the two spec files torun-server-tests.shso the resque/redis/authenticated-mongo environment runs them too (about 1s, before the feature specs, leaves the db empty).0x1Abyte. It passes on Linux/macOS; the validator's acceptance of that exact fixture is also covered directly by a model spec.Deferred (from the issue's proposals)
🤖 Generated with Claude Code