Skip to content

[ZEPPELIN-6551] Fix Job Manager crash on removal broadcast for a note not in the viewer's list#5337

Open
kimyenac wants to merge 3 commits into
apache:masterfrom
kimyenac:ZEPPELIN-6551
Open

[ZEPPELIN-6551] Fix Job Manager crash on removal broadcast for a note not in the viewer's list#5337
kimyenac wants to merge 3 commits into
apache:masterfrom
kimyenac:ZEPPELIN-6551

Conversation

@kimyenac

Copy link
Copy Markdown
Contributor

What is this PR for?

When a note is permanently deleted, the backend broadcasts a field-less NoteJobInfo stub (noteName=null, isRemoved=true) to every Job Manager subscriber, while each viewer's initial job list is owner-filtered (JobManagerService#getNoteJobInfoByUnixTime).

For a note the viewer does not own, updateJobs fell into the currentJobIndex === -1 branch and pushed the stub into this.jobs. filterJobs then ran job.noteName.match(noteNameReg) and threw TypeError: Cannot read properties of undefined (reading 'match'). Because the stub persists in this.jobs, every subsequent filterJobs (each filter keystroke / job update) rethrew, so the page's filter/sort stayed broken until re-navigation.

This affects any user viewing /jobmanager when another user permanently deletes a note they don't own.

What does this PR do?

  • updateJobs: do not add removal stubs that aren't already in the list (if (!updateJob.isRemoved)). The guard is kept nested inside if (currentJobIndex === -1) rather than merged into that condition, so a removal stub can never fall through to the else branch's splice(currentJobIndex, 1) — merging would call splice(-1, 1) and silently drop the last job.
  • filterJobs: guard job.noteName?.match(...) with optional chaining as defense in depth.

What type of PR is it?

Bug Fix

What is the Jira issue?

How should this be tested?

  • cd zeppelin-web-angular && npm run lint
  • With two users: open /jobmanager as user A; as user B permanently delete a note A does not own; confirm A's page keeps filtering without a console TypeError.

Questions:

  • Does the license files need to update? No
  • Is there breaking changes for older versions? No
  • Does this needs documentation? No

@jongyoul

Copy link
Copy Markdown
Member

@kimyenac Can we add test cases for this issue? :-)

@kimyenac
kimyenac force-pushed the ZEPPELIN-6551 branch 2 times, most recently from 511489a to fb091bf Compare July 25, 2026 08:55
@kimyenac

Copy link
Copy Markdown
Contributor Author

@jongyoul Thanks for the review! Added e2e regression tests in job-manager-removal-broadcast.spec.ts (5 cases).

Reverting the fix makes two of them fail with the reported TypeError: Cannot read properties of undefined (reading 'match') and a filter that stops responding to input. A third guards against folding the removal check into the currentJobIndex === -1 condition, which would splice(-1, 1) the last job out of the list.

The Job Manager's WebSocket feed is stubbed in the test because zeppelin.jobmanager.enable defaults to false and getNoteJobInfoByUnixTime is owner-filtered, so the cross-user removal broadcast cannot be produced from a single session. All other WebSocket traffic still goes to the real server.

@voidmatcha voidmatcha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for feedback! One suggestion.

test 1 and test 3 assert toHaveCount(2) right after the removal, but the list already has 2 items, so they pass on a state that is true before the removal is processed. On a regressed build filterJobs throws and the DOM stays frozen at 2, so the assertion stays true for the whole timeout and never catches it. The error check is one-shot too.

Sending a sentinel job after the removal (WebSocket delivery is FIFO) and awaiting its render guarantees the removal ran first, so both tests actually exercise the regression. (test 2 already has a barrier via the search input.)

a3afb74

kimyenac added 3 commits July 26, 2026 14:16
… not in the viewer's list

A permanent note deletion broadcasts a field-less NoteJobInfo stub
(noteName=null, isRemoved=true) to every Job Manager subscriber, while
each viewer's job list is owner-filtered. For a note the viewer doesn't
own, updateJobs pushed the stub (currentJobIndex === -1), then filterJobs
threw "Cannot read properties of undefined (reading 'match')" on
job.noteName and kept rethrowing, breaking filter/sort until re-navigation.

- updateJobs: don't add removal stubs that aren't already in the list.
  Guard is nested (not merged into the currentJobIndex === -1 condition)
  so a removal stub can't fall through to the splice(-1) branch.
- filterJobs: guard job.noteName with optional chaining as defense in depth.
…broadcast

Cover the removal broadcast that `NotebookServer#onNoteRemove` sends to every Job
Manager subscriber: a removal stub for a note absent from the viewer's list must
be ignored, must not break the note name filter, and must not drop a listed job.

The Job Manager's WebSocket traffic is stubbed because
`zeppelin.jobmanager.enable` defaults to false and the backend list is
owner-filtered, so the cross-user broadcast cannot be produced from a single
session. All other WebSocket traffic still goes to the real server.
…egression

Ignoring a removal stub leaves the job list untouched, so the assertions that
ran right after the broadcast were already satisfied by the pre-broadcast state,
and a regressed build freezes the rendered list in that same state, so they
could not fail. With the fix reverted the previous spec still passed the
"no listed job is dropped" test outright, and the runtime-error test passed or
failed depending on when the pageerror happened to land.

Broadcast a sentinel job after the removal and wait for it to render instead.
Both messages travel the same socket in FIFO order into the same listener, so
the sentinel appearing proves the removal was handled first; on a regressed
build filterJobs keeps throwing and it never appears. The note name filter test
already had that barrier via the search box, so only its redundant count
assertion is dropped.

Reverting the fix now fails 3 of the 5 tests, and so does folding the removal
guard into the currentJobIndex === -1 condition.
@kimyenac

kimyenac commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

@voidmatcha Good catch. With the fix reverted, the old spec passed "no listed job is dropped" outright, and the runtime-error test was a coin flip: run alone it passed in 760ms, before the pageerror landed.

Fixed in 8007d2247. Both tests now broadcast a sentinel job after the removal and wait for it to render. Same socket, FIFO, same listener, so the sentinel appearing proves the removal was handled. On a regressed build filterJobs keeps throwing, so it never appears at all. The counts became toHaveCount(3) (Alpha + Beta + sentinel), which also pins down that the stub was dropped. The filter test keeps its search-box barrier; only its redundant count assertion is gone.

Verified on chromium: 5/5 pass with the fix, tests 1-3 fail with it reverted, and the same 3 fail if the guard is folded into currentJobIndex === -1.

@voidmatcha voidmatcha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM 👍

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.

3 participants