Skip to content

fix(amber): report a malformed PVE websocket handshake - #7852

Merged
aglinxinyuan merged 1 commit into
apache:mainfrom
aglinxinyuan:fix/pve-websocket-handshake
Aug 23, 2026
Merged

fix(amber): report a malformed PVE websocket handshake#7852
aglinxinyuan merged 1 commit into
apache:mainfrom
aglinxinyuan:fix/pve-websocket-handshake

Conversation

@aglinxinyuan

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

PveWebsocketResource.onOpen read the handshake parameters in its prologue, outside the Future that owns both the catch arm that turns a failure into an [ERR] line and the pump that writes to the socket. A handshake missing cuid, carrying an empty value list for it, or carrying a non-numeric one threw out of onOpen itself, ahead of both — so the endpoint's own error contract could not see it:

Before:  ?pveName=env&action=install  ->  NumberFormatException out of onOpen  ->  socket closes, nothing sent
After:   ?pveName=env&action=install  ->  [ERR] Missing required parameter: cuid  ->  __DONE__

The client side makes that silent close worse than a lost message: computing-unit-selection.component.ts clears isInstalling/isLocked only when onmessage sees __DONE__, and there is no onclose handler, so the pip modal stayed locked on "installing" with an empty log until the user navigated away.

The three reads move inside the try, so a malformed handshake now travels the same path as any other failure — an [ERR] line followed by the sentinel. A requiredParam helper rejects an absent key, an empty value list, and a blank value alike; the last one is reachable from the wire, since ?pveName= arrives as [""] and used to resolve to a venv directory named " " rather than failing.

Handshake Before After
no cuid NPE out of onOpen, socket closes empty [ERR] Missing required parameter: cuid then __DONE__
cuid present, value list empty IndexOutOfBoundsException, same same as above
cuid=abc NumberFormatException, same [ERR] Invalid cuid: abc then __DONE__
pveName= (blank) resolved a venv path with a whitespace name [ERR] Missing required parameter: pveName then __DONE__

Well-formed handshakes are untouched: the parsed cuid/pveName reach PveManager exactly as before.

Any related issues, documentation, discussions?

Follows #7847, which added PveWebsocketResourceSpec and deliberately left this path unpinned so that fixing it would not have to fight a test that had cemented it.

How was this PR tested?

Four cases added to PveWebsocketResourceSpeccuid absent, cuid with an empty value list, a non-numeric cuid, and a blank pveName — each asserting the client receives both the [ERR] line and the sentinel, and that the pump stops rather than parking in queue.take().

sbt "WorkflowExecutionService/testOnly org.apache.texera.web.resource.pythonvirtualenvironment.PveWebsocketResourceSpec"

Tests: succeeded 7, failed 0. Written before the fix: all four failed against the old source with the escaping exception itself (NullPointerException, IndexOutOfBoundsException, NumberFormatException) rather than an assertion mismatch, which is the defect stated as a test. Hoisting the reads back out of the Future fails all four again.

Mutation testing on the new code — 4 mutants, 4 killed: dropping the values == null arm, dropping the isEmpty arm, hardcoding cuid into the message instead of interpolating the parameter name, and swallowing a bad cuid as 0.

PveResourceSpec, the other suite in the package, is unaffected — locally it reports 6 pre-existing failures that all come from the Windows interpreter path (Scripts\python.exe), unrelated to the endpoint.

sbt scalafmtCheckAll "scalafixAll --check"

Clean.

Was this PR authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 5)

onOpen read cuid and pveName in its prologue, outside the Future that owns
the catch arm and the pump. A handshake with no cuid, an empty value list
for it, or a non-numeric one therefore threw out of onOpen itself, ahead of
both: no [ERR] line, no __DONE__ sentinel, just a socket that closes. The
frontend clears isInstalling only on the sentinel in onmessage and has no
onclose handler, so the pip modal stayed locked on "installing" with an
empty log.

The reads move inside the try, and a requiredParam helper rejects an absent
key, an empty value list and a blank value alike: ?pveName= reaches the
endpoint as [""], which used to resolve to a venv directory named "   ".

Four cases added to PveWebsocketResourceSpec, each failing against the old
code with the escaping exception rather than an assertion mismatch. 4
mutations, 4 killed: dropping either arm of the presence guard, hardcoding
cuid into the message instead of interpolating the parameter name, and
swallowing a bad cuid as 0. Hoisting the reads back out of the Future fails
all four tests.
Copilot AI lite review requested due to automatic review settings August 22, 2026 23:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • Contributors with relevant context: @SarahAsad23
    You can notify them by mentioning @SarahAsad23 in a comment.

@Yicong-Huang Yicong-Huang added the release/v1.2 back porting to release/v1.2 label Aug 22, 2026
@github-actions
github-actions Bot requested a review from xuang7 August 22, 2026 23:53
@github-actions

Copy link
Copy Markdown
Contributor

Backport auto-label report

This fix: PR was checked against each actively-supported release branch. release/* labels drive the post-merge backport, so add or remove one to change where this fix lands.

Release branch Analysis
release/v1.2 Change detected on this branch — label added; this fix is queued to backport here. Requested review from @xuang7.

Auto-label run.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Benchmark changes need a look

🟢 2 better · 🔴 7 worse · ⚪ 6 noise (<±5%) · 0 without baseline

Compared against main ce0510d benchmarked on this same runner, so the delta is largely free of cross-runner hardware noise. The "7d avg" column still reflects the gh-pages dashboard. Treat <±5% as noise unless repeated.

Dashboard · Run

config throughput MB/s latency max Δ latest / 7d
🔴 bs=10 sw=10 sl=64 379 0.231 26,325/30,726/30,726 us 🔴 +18.2% / 🔴 +121.9%
🔴 bs=100 sw=10 sl=64 780 0.476 125,132/170,955/170,955 us 🔴 +16.8% / 🔴 +71.7%
🟢 bs=1000 sw=10 sl=64 927 0.566 1,074,635/1,135,577/1,135,577 us 🟢 -5.8% / 🔴 +19.2%
Baseline details

Latest main ce0510d from same runner

config metric PR latest main 7d avg Δ latest Δ 7d
bs=10 sw=10 sl=64 throughput 379 tuples/sec 418 tuples/sec 833.79 tuples/sec -9.3% -54.5%
bs=10 sw=10 sl=64 MB/s 0.231 MB/s 0.255 MB/s 0.509 MB/s -9.4% -54.6%
bs=10 sw=10 sl=64 p50 26,325 us 22,270 us 11,864 us +18.2% +121.9%
bs=10 sw=10 sl=64 p95 30,726 us 30,349 us 14,381 us +1.2% +113.7%
bs=10 sw=10 sl=64 p99 30,726 us 30,349 us 18,035 us +1.2% +70.4%
bs=100 sw=10 sl=64 throughput 780 tuples/sec 827 tuples/sec 1,083 tuples/sec -5.7% -28.0%
bs=100 sw=10 sl=64 MB/s 0.476 MB/s 0.505 MB/s 0.661 MB/s -5.7% -28.0%
bs=100 sw=10 sl=64 p50 125,132 us 119,434 us 93,077 us +4.8% +34.4%
bs=100 sw=10 sl=64 p95 170,955 us 146,336 us 99,553 us +16.8% +71.7%
bs=100 sw=10 sl=64 p99 170,955 us 146,336 us 108,604 us +16.8% +57.4%
bs=1000 sw=10 sl=64 throughput 927 tuples/sec 917 tuples/sec 1,119 tuples/sec +1.1% -17.1%
bs=1000 sw=10 sl=64 MB/s 0.566 MB/s 0.56 MB/s 0.683 MB/s +1.1% -17.1%
bs=1000 sw=10 sl=64 p50 1,074,635 us 1,078,873 us 909,247 us -0.4% +18.2%
bs=1000 sw=10 sl=64 p95 1,135,577 us 1,205,047 us 952,561 us -5.8% +19.2%
bs=1000 sw=10 sl=64 p99 1,135,577 us 1,205,047 us 985,186 us -5.8% +15.3%
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,527.56,200,128000,379,0.231,26325.39,30726.09,30726.09
1,100,10,64,20,2562.74,2000,1280000,780,0.476,125132.04,170955.37,170955.37
2,1000,10,64,20,21586.02,20000,12800000,927,0.566,1074635.41,1135576.99,1135576.99

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.96%. Comparing base (ce0510d) to head (63016f4).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #7852      +/-   ##
============================================
- Coverage     91.98%   91.96%   -0.02%     
  Complexity     4510     4510              
============================================
  Files          1173     1173              
  Lines         47346    47347       +1     
  Branches       5305     5306       +1     
============================================
- Hits          43550    43544       -6     
- Misses         2155     2159       +4     
- Partials       1641     1644       +3     
Flag Coverage Δ *Carryforward flag
access-control-service 81.00% <ø> (ø) Carriedforward from ce0510d
agent-service 98.62% <ø> (ø) Carriedforward from ce0510d
amber 88.41% <100.00%> (-0.04%) ⬇️
computing-unit-managing-service 73.67% <ø> (ø) Carriedforward from ce0510d
config-service 86.73% <ø> (ø) Carriedforward from ce0510d
file-service 75.74% <ø> (ø) Carriedforward from ce0510d
frontend 93.99% <ø> (ø) Carriedforward from ce0510d
notebook-migration-service 79.13% <ø> (ø) Carriedforward from ce0510d
pyamber 97.52% <ø> (ø) Carriedforward from ce0510d
workflow-compiling-service 77.19% <ø> (ø) Carriedforward from ce0510d

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xuang7 xuang7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@aglinxinyuan
aglinxinyuan added this pull request to the merge queue Aug 23, 2026
Merged via the queue into apache:main with commit c9a0ddf Aug 23, 2026
27 of 33 checks passed
@aglinxinyuan
aglinxinyuan deleted the fix/pve-websocket-handshake branch August 23, 2026 04:50
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened: draft #7866 (#7866) to release/v1.2, assigned to @aglinxinyuan — needs manual work because the cherry-pick conflicts.

aglinxinyuan added a commit that referenced this pull request Aug 23, 2026
### What changes were proposed in this PR?

`PveWebsocketResource.onOpen` read the handshake parameters in its
prologue, outside the `Future` that owns both the `catch` arm that turns
a failure into an `[ERR]` line and the pump that writes to the socket. A
handshake missing `cuid`, carrying an empty value list for it, or
carrying a non-numeric one threw out of `onOpen` itself, ahead of both —
so the endpoint's own error contract could not see it:

```
Before:  ?pveName=env&action=install  ->  NumberFormatException out of onOpen  ->  socket closes, nothing sent
After:   ?pveName=env&action=install  ->  [ERR] Missing required parameter: cuid  ->  __DONE__
```

The client side makes that silent close worse than a lost message:
`computing-unit-selection.component.ts` clears `isInstalling`/`isLocked`
only when `onmessage` sees `__DONE__`, and there is no `onclose`
handler, so the pip modal stayed locked on "installing" with an empty
log until the user navigated away.

The three reads move inside the `try`, so a malformed handshake now
travels the same path as any other failure — an `[ERR]` line followed by
the sentinel. A `requiredParam` helper rejects an absent key, an empty
value list, and a blank value alike; the last one is reachable from the
wire, since `?pveName=` arrives as `[""]` and used to resolve to a venv
directory named `" "` rather than failing.

| Handshake | Before | After |
| --- | --- | --- |
| no `cuid` | NPE out of `onOpen`, socket closes empty | `[ERR] Missing
required parameter: cuid` then `__DONE__` |
| `cuid` present, value list empty | IndexOutOfBoundsException, same |
same as above |
| `cuid=abc` | NumberFormatException, same | `[ERR] Invalid cuid: abc`
then `__DONE__` |
| `pveName=` (blank) | resolved a venv path with a whitespace name |
`[ERR] Missing required parameter: pveName` then `__DONE__` |

Well-formed handshakes are untouched: the parsed `cuid`/`pveName` reach
`PveManager` exactly as before.

### Any related issues, documentation, discussions?

Follows #7847, which added `PveWebsocketResourceSpec` and deliberately
left this path unpinned so that fixing it would not have to fight a test
that had cemented it.

### How was this PR tested?

Four cases added to `PveWebsocketResourceSpec` — `cuid` absent, `cuid`
with an empty value list, a non-numeric `cuid`, and a blank `pveName` —
each asserting the client receives both the `[ERR]` line and the
sentinel, and that the pump stops rather than parking in `queue.take()`.

```bash
sbt "WorkflowExecutionService/testOnly org.apache.texera.web.resource.pythonvirtualenvironment.PveWebsocketResourceSpec"
```

`Tests: succeeded 7, failed 0`. Written before the fix: all four failed
against the old source with the escaping exception itself
(`NullPointerException`, `IndexOutOfBoundsException`,
`NumberFormatException`) rather than an assertion mismatch, which is the
defect stated as a test. Hoisting the reads back out of the `Future`
fails all four again.

Mutation testing on the new code — 4 mutants, 4 killed: dropping the
`values == null` arm, dropping the `isEmpty` arm, hardcoding `cuid` into
the message instead of interpolating the parameter name, and swallowing
a bad `cuid` as `0`.

`PveResourceSpec`, the other suite in the package, is unaffected —
locally it reports 6 pre-existing failures that all come from the
Windows interpreter path (`Scripts\python.exe`), unrelated to the
endpoint.

```bash
sbt scalafmtCheckAll "scalafixAll --check"
```

Clean.

Backport notes -- two places where `release/v1.2` differs from the source commit's
branch, so the cherry-pick did not apply as-is:

- `PveManager.createNewPve`/`installUserPackages` still take an `isLocal` flag here, so
  `val isLocal = !KubernetesConfig.kubernetesComputingUnitEnabled` stays in `onOpen`'s
  prologue rather than moving into the `try` with the other three reads. It reads
  configuration, not the handshake, so none of the failure modes this commit fixes can
  originate from it, and the endpoint's error contract is unchanged by where it sits.
- The spec's `pythonBinFor` helper drops the source commit's Windows
  `Scripts/python.exe` arm. This branch's `PveManager.pythonBinPath` resolves
  `bin/python` unconditionally -- the platform split is a later change to `PveManager`
  that this backport does not carry -- so keeping the arm would assert an interpreter
  path the endpoint under test cannot produce.

### Was this PR authored or co-authored using generative AI tooling?

(backported from commit c9a0ddf)

Generated-by: Claude Code (Opus 5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine fix release/v1.2 back porting to release/v1.2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants