[ZEPPELIN-6579] Make notebook tree reload safe for concurrent note operations - #5357
Conversation
|
Thanks for fixing the mixed-generation lookup failure. I narrowed the title to match this focused scope: atomically publishing one fully built notebook-tree state during reload. Please address writer-side coordination in a follow-up PR: concurrent reloads, and reload racing with add/save/move/remove. That follow-up should include deterministic latch-based tests and account for the overlap with #5325. The current approval remains for this focused fix. |
Thanks for the review!! Should I open a follow-up JIRA ticket for writer-side coordination (concurrent reloads and race conditions with add/save/move/remove) and handle it in a separate PR? |
|
Merged into master (d8c43cb). |
What is this PR for?
NoteManagerlocates a note through two separate pieces of state:notesInfomaps a note id to its path, androotholds the folder tree that the path is walked against. A lookup uses both in sequence, so the two have to agree.reloadNotes()replaced them one at a time:Neither field is
volatileand nothing is held while they are swapped, so a concurrentprocessNote()can observe a mapping and a tree that belong to different generations:notesInfo.containsKey(noteId)passesThe guard in
processNote()only checksnotesInfo, so it passes and the failure surfaces one line later ingetNoteNode():IOExceptionis not mapped to a specific status, soWebApplicationExceptionMapperturns it into HTTP 500 for a note that was never removed. Everything that goes throughprocessNote()is affected: reading a note, updating a paragraph, creating, deleting and moving notes, and listing the notebook.This PR holds the tree, the trash folder and the mapping in one immutable
NoteTreeand publishes it with a singlevolatilewrite.buildNoteTree()fills the new tree locally and returns it; only then is it assigned. The tree-walking helpers (getNoteNode,getFolder,getOrCreateFolder,isNotePathAvailable) take the tree as a parameter, and callers that need both pieces of state read the reference once, so a lookup resolves the mapping and the tree against the same generation. Those helpers arestaticso that the compiler prevents them from reaching back to the field.Scope and related issues
#5325 (
[ZEPPELIN-5858]) is open against the same class and restructuresremoveNote,moveNoteandmoveFolderwithsynchronized (this). It targets a different race (two mutators duplicating a note) and its monitor does not coverreloadNotes(), so neither change subsumes the other. Whichever merges second will need a rebase.What type of PR is it?
Bug Fix
Todos
buildNoteTree()before publishing themNoteTreepublished through a singlevolatilewriteWhat is the Jira issue?
How should this be tested?
New test
NoteManagerTest#testConcurrentReloadAndProcessNote: it saves 50 notes, then runsreloadNotes()in a loop on one thread while four threads keep callingprocessNote()for every note, and asserts that no lookup fails or returns nothing.Result with the fix:
Tests run: 7, Failures: 0, Errors: 0.Reverting only the production change makes the new test fail on every reader thread with
java.io.IOException: Can not find note: /prod/note_0thrown fromNoteManager.getNoteNodeviaNoteManager.processNote, which is the stack from the ticket; with the fix it passes.Also run, to cover the callers of the reload path:
./mvnw package -pl zeppelin-server --am \ -Dtest='NotebookTest#testReloadAllNotes+testReloadAndSetInterpreter' -DfailIfNoTests=falseResult:
Tests run: 2, Failures: 0, Errors: 0.Not verified locally: the full
NotebookTestandNotebookServerTestclasses, which start real remote interpreter processes and time out in my environment, andNotebookRepoSyncTest. Those are left to CI.Screenshots (if appropriate)
N/A
Questions: